From 0a9d6dfe3115a7c057cc78574128427e184b7620 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 22:24:03 +0200 Subject: [PATCH 1/5] Add an example package (#125) * Add an example package * Clean up --- example_package/.gitignore | 76 +++ {examples => example_package}/config.yml | 24 +- example_package/in/.gitkeep | 0 example_package/out/.gitkeep | 0 example_package/prog/abc.cpp | 12 + example_package/prog/abcingen.cpp | 45 ++ example_package/prog/abcingen.sh | 42 ++ example_package/prog/abcinwer.cpp | 56 ++ example_package/prog/abcs.cpp | 11 + example_package/prog/oi.h | 651 ++++++++++++++++++++++ src/sinol_make/commands/inwer/__init__.py | 2 +- 11 files changed, 907 insertions(+), 12 deletions(-) create mode 100644 example_package/.gitignore rename {examples => example_package}/config.yml (94%) create mode 100644 example_package/in/.gitkeep create mode 100644 example_package/out/.gitkeep create mode 100644 example_package/prog/abc.cpp create mode 100644 example_package/prog/abcingen.cpp create mode 100644 example_package/prog/abcingen.sh create mode 100644 example_package/prog/abcinwer.cpp create mode 100644 example_package/prog/abcs.cpp create mode 100644 example_package/prog/oi.h diff --git a/example_package/.gitignore b/example_package/.gitignore new file mode 100644 index 00000000..1e728bc9 --- /dev/null +++ b/example_package/.gitignore @@ -0,0 +1,76 @@ +# sinol-make +.cache +cache +in/.md5sums + +# Tests +in/*.in +out/*.out +!in/???0*.in +!out/???0*.out + +# export package file +*.tgz + +# LaTeX +*.pdf +*.ps +!doc/logo.* + +*.aux +*.lof +*.log +*.lot +*.fls +doc/*.out +*.toc +*.fmt +*.fot +*.cb +*.cb2 +.*.lb + +*.dvi +*.xdv +*-converted-to.* +# these rules might exclude image files for figures etc. +*.eps + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.run.xml + +# Encrypted files +*.gpg + +# SIO binnary +*.e + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# pyenv +.python-version + +# IPython +profile_default/ +ipython_config.py + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so +*.o diff --git a/examples/config.yml b/example_package/config.yml similarity index 94% rename from examples/config.yml rename to example_package/config.yml index 21baee08..b6228d8c 100644 --- a/examples/config.yml +++ b/example_package/config.yml @@ -13,10 +13,8 @@ title_pl: Przykładowe zadanie # (if number of groups doesn't divide 100, then the last groups will have the remaining points). # Group 0 always has zero points. scores: - 1: 20 - 2: 30 - 3: 25 - 4: 25 + 1: 40 + 2: 60 # Time limit for all tests is defined in `time_limit` key. # More precise time limit for each group or test can be defined in `time_limits` key. @@ -43,18 +41,22 @@ override_limits: # Extra compilation arguments can be defined in `extra_compile_args` key. # Each language can have different extra arguments. -extra_compilation_args: - cpp: 'abclib.cpp' + +# extra_compilation_args: +# cpp: 'abclib.cpp' + # The arguments can also be in an array: -extra_compilation_args: - cpp: - - 'abclib.cpp' - - 'abclib2.cpp' + +# extra_compilation_args: +# cpp: +# - 'abclib.cpp' +# - 'abclib2.cpp' # Additional files used in compilation can be defined in `extra_compilation_files` key. # They are copied to the directory where the source code is compiled. # All languages have the same additional files. -extra_compilation_files: ['abclib.cpp', 'abclib.py'] + +# extra_compilation_files: ['abclib.cpp', 'abclib.py'] ### Keys used by sinol-make: diff --git a/example_package/in/.gitkeep b/example_package/in/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/example_package/out/.gitkeep b/example_package/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/example_package/prog/abc.cpp b/example_package/prog/abc.cpp new file mode 100644 index 00000000..2d1f3b2f --- /dev/null +++ b/example_package/prog/abc.cpp @@ -0,0 +1,12 @@ +// This is the main model solution. +// It is used for generating output files. + +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + cout << a + b << endl; +} diff --git a/example_package/prog/abcingen.cpp b/example_package/prog/abcingen.cpp new file mode 100644 index 00000000..b5a0f7db --- /dev/null +++ b/example_package/prog/abcingen.cpp @@ -0,0 +1,45 @@ +#include +#include "oi.h" +using namespace std; + +// Change this function to generate one test for stresstesting. +// The script prog/abcingen.sh in 10 seconds generates +// as much tests as possible and compares the outputs +// of the model solution and brute solution. +// The tests shouldn't be very big, but should be able to cover edge cases. +void generate_one_stresstest(oi::Random &rng) { + cout << rng.randSInt(1, 10) << ' ' << rng.randSInt(1, 10) << endl; +} + +// Change this function to create a test with the given name. +// The lists of tests to generate needs to be written in prog/abcingen.sh +void generate_proper_test(string test_name, oi::Random &rng) { + if (test_name == "0a") + cout << "0 1" << endl; + else if (test_name == "1a") + cout << rng.randSInt(5, 1'000) << ' ' << rng.randSInt(5, 1'000) << endl; + else if (test_name == "2a") + cout << "2 2" << endl; + else { + cerr << "Unrecognized test_name = " << test_name << endl; + exit(1); + } +} + +int main(int argc, char *argv[]) { + if (argc == 3 && string(argv[1]) == "stresstest") { + unsigned int seed = atoi(argv[2]); + oi::Random rng(seed); + generate_one_stresstest(rng); + return 0; + } + if (argc != 2) { + cerr << "Run prog/abcingen.sh to stresstest and create proper tests." << endl; + exit(1); + } + string test_name = argv[1]; + unsigned int seed = (unsigned int) hash{}(test_name); + oi::Random rng(seed); + cerr << "Generating test " << test_name << "..." << endl; + generate_proper_test(test_name, rng); +} diff --git a/example_package/prog/abcingen.sh b/example_package/prog/abcingen.sh new file mode 100644 index 00000000..ad7b6e87 --- /dev/null +++ b/example_package/prog/abcingen.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# This script first stresstests the model solution for 10 seconds +# and if it passes, it will generate the proper tests. +# To generate both types of tests, it executes ingen.cpp and passes it some arguments. +# The `test_ids` variable needs to have a manually written list of all proper tests. + +prog_dir="$(realpath "$(dirname "$0")")" +cache_dir="$prog_dir/../.cache" +mkdir -p "$cache_dir" +script_name="$(basename "$0")" +task_id=${script_name:0:3} +gen_exe="$cache_dir/${task_id}ingen" +sol_exe="$cache_dir/${task_id}solution" +slo_exe="$cache_dir/${task_id}slow" +stresstest_seconds=10 +function compile_cpp { + g++ -std=c++20 -O3 -lm -Werror -Wall -Wextra -Wshadow -Wconversion -Wno-unused-result -Wfloat-equal "$1" -o "$2" \ + || exit 1 +} + +# Change the list of tests to generate and (if needed) the paths of solutions. +test_ids="0a 1a 2a" +compile_cpp "$prog_dir/${task_id}ingen.cpp" "$gen_exe" +compile_cpp "$prog_dir/${task_id}.cpp" "$sol_exe" +compile_cpp "$prog_dir/${task_id}s.cpp" "$slo_exe" + +for (( i=0, SECONDS=0; SECONDS < stresstest_seconds; i++ )); do + in_test="$cache_dir/input.in" + slo_out="$cache_dir/slo.out" + sol_out="$cache_dir/sol.out" + printf "Running stresstest $i\r" + "$gen_exe" stresstest $i > "$in_test" || { echo "Failed to generate test $i"; exit 1; } + "$slo_exe" < "$in_test" > "$slo_out" || { echo "Brute crashed on test $i"; exit 1; } + "$sol_exe" < "$in_test" > "$sol_out" || { echo "Solution crashed on test $i"; exit 1; } + diff "$slo_out" "$sol_out" -w > /dev/null || { echo "Outputs differ on test $i"; exit 1; } +done +echo "Stresstest passed with $i tests" + +for test in $test_ids; do + "$gen_exe" "$test" > "$prog_dir/../in/${task_id}${test}.in" || { echo "Failed to generate test $test"; exit 1; } +done diff --git a/example_package/prog/abcinwer.cpp b/example_package/prog/abcinwer.cpp new file mode 100644 index 00000000..2bfb3a5f --- /dev/null +++ b/example_package/prog/abcinwer.cpp @@ -0,0 +1,56 @@ +#include +#include "oi.h" +using namespace std; + +int main() { + oi::Scanner in(stdin, oi::PL); + + // Change this code to read and validate the input. + int n = in.readInt(0, 1'000); + in.readSpace(); + int m = in.readInt(0, 1'000); + in.readEoln(); + in.readEof(); + assert(n > 0 || m > 0); + + // Change this code to have functions which return + // whether the test satisfies a given subtask. + auto is_subtask1 = [&]() -> bool { + return n >= 0 && m >= 0; + }; + auto is_subtask2 = [&]() -> bool { + return n >= 5 && m >= 5; + }; + + // Change this code to have functions which return + // whether the test is exactly the same as + // the sample tests in the statement. + auto is_0a = [&]() -> bool { + return n == 0 && m == 1; + }; + auto is_1ocen = [&]() -> bool { + return n == 1000 && m == 1000; + }; + + map subtasks = { + {1, is_subtask1()}, + {2, is_subtask2()}, + }; + string subtasks_s; + for (auto [subtask_id, is_valid] : subtasks) + subtasks_s += is_valid ? to_string(subtask_id) : string("-"); + + map sample_tests = { + {"0a", is_0a()}, + {"1ocen", is_1ocen()}, + }; + string sample_test_s = "-"; + for (auto [name, is_valid] : sample_tests) + if (is_valid) + sample_test_s = name; + + cout << "OK " + << "n = " << setw(4) << n << ", " + << "m = " << setw(4) << m << ", " + << "subtasks = " << subtasks_s << ", sample test = " << sample_test_s << endl; +} diff --git a/example_package/prog/abcs.cpp b/example_package/prog/abcs.cpp new file mode 100644 index 00000000..b25a5dff --- /dev/null +++ b/example_package/prog/abcs.cpp @@ -0,0 +1,11 @@ +// This is a "brute force" solution for testing model solution. + +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + cout << a + b << endl; +} diff --git a/example_package/prog/oi.h b/example_package/prog/oi.h new file mode 100644 index 00000000..3dc11bd8 --- /dev/null +++ b/example_package/prog/oi.h @@ -0,0 +1,651 @@ +/* + oi.h - pakiet funkcji do pisania weryfikatorow wejsc (inwer) i wyjsc (chk) + Pierwotny autor: Piotr Niedzwiedz + W razie problemow, bledow lub pomyslow na ulepszenie prosze pisac issues: https://sinol3.dasie.mimuw.edu.pl/sinol3/template-package +*/ + +#ifndef OI_LIB_OI_H_ +#define OI_LIB_OI_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +using std::vector; +using std::max; +using std::swap; + +// We want prevent usage of standard random function. +int rand(){ + fprintf(stderr, "DONT USE rand or random_shuffle!\nUse oi::Random class.\n"); + exit(1); + return 0; +} + +namespace oi { + +enum Lang { + EN = 0, + PL = 1 +}; + +class Reader; + +class Scanner { + protected: + static const int realNumbersLimit = 20; + + Lang lang; + Reader* reader; + void(*end)(const char* msg, int line, int position); + + void readULL(unsigned long long int limit, unsigned long long int &val, bool &sign); + void readLDB(long double &val, bool &sign); + + public: + Scanner(const char* file, Lang _lang = Lang(EN)); + Scanner(const char* file, void(*endf)(const char* msg, int line, int position), Lang _lang = Lang(EN)); + Scanner(FILE* input, Lang _lang = Lang(EN)); + Scanner(FILE* input, void(*endf)(const char* msg, int line, int position), Lang _lang = Lang(EN)); + ~Scanner(); + + void error(const char* msg); + + // Skips all whitespaces until any occurrence of EOF or other non-white character. + int skipWhitespaces(); + // Skips all whitespaces until any occurrence of EOF, EOLN or other non-white character. + int skipWhitespacesUntilEOLN(); + + int readInt(int min_value = INT_MIN, int max_value = INT_MAX); + unsigned int readUInt(unsigned int min_value = 0, unsigned int max_value = UINT_MAX); + long long readLL(long long int min_value = LLONG_MIN, long long int max_value = LLONG_MAX); + unsigned long long readULL(unsigned long long int min_value = 0, unsigned long long int max_value = ULLONG_MAX); + + float readFloat(float min_value, float max_value); + double readDouble(double min_value, double max_value); + long double readLDouble(long double min_value, long double max_value); + + char readChar(); + + // Newline character is read, but isn't added to s + int readLine(char* s, int size); + + // Reads a string until occurrence of EOF, EOLN or whitespace. + // Returns the number of characters read (possibly 0). + int readString(char* s, int size); + + void readEof(); + void readEofOrEoln(); + void readEoln(); + void readSpace(); + void readTab(); + + bool isEOF(); + private: + Scanner(Scanner&) {} +}; + +class MultipleRecursiveGenerator; + +class Random { + public: + Random(); + explicit Random(unsigned int seed); + ~Random(); + void setSeed(unsigned int seed); + + // Random number from range <0..2^31 - 1> + int rand(); + + // randS* - signed * + // randU* - unsigned * + int randSInt(); + unsigned int randUInt(); + long long randSLL(); + unsigned long long randULL(); + + /** + * Generuje liczbe pseudo-losowa z przedzialu [a..b] (obustronnie wlacznie). + * Podanie na wejsciu pustego przedzialu (b < a) jest bledem i skutkuje + * dzialaniem niezdefiniowanym. + */ + int randSInt(const int a, const int b); + + template + void randomShuffle(RandomAccessIterator first, RandomAccessIterator last); + + private: + Random(Random&) {} + MultipleRecursiveGenerator* mrg_; + void init(); +}; + +class MultipleRecursiveGenerator { + public: + MultipleRecursiveGenerator(unsigned int modulo, + const vector &A); + void setSeed(unsigned int seed); + + /** + * Generuje liczbe pseudo-losowa z przedzialu [0..modulo-1]. Chcemy zeby to + * bylo co najmniej 16 bitow, czyli musi byc (0xFFFF < modulo). Gwarantuje + * to asercja w konstruktorze klasy. + */ + unsigned int next16Bits(); + + private: + long int n_; + unsigned int modulo_; + vector A_; + vector X_; +}; + +class Reader { + private: + static const int bufferSize = 1000; + char Buffer[bufferSize]; + int head, tail; + int line, position; + void fillBuffer(); + FILE* input; + public: + explicit Reader(const char* file); + explicit Reader(FILE* _input); + ~Reader(); + bool isEOF(); + int getLine() {return line;} + int getPosition() {return position;} + char read(bool move = false); + private: + Reader(Reader&) {}; +}; + + +const char* msgLeadingZeros[]= { + "Leading zeros", + "Zera wiodace"}; +const char* msgMinusZero[]= { + "Minus zero -0", + "Minus zero -0"}; +const char* msgNoNumber[]= { + "No number", + "Brak liczby"}; +const char* msgNoChar[]= { + "No char - EOF", + "Brak znaku - EOF"}; +const char* msgNotEof[]= { + "Not EOF", + "Brak konca pliku"}; +const char* msgNotEoln[]= { + "Not EOLN", + "Brak konca linii"}; +const char* msgNotEofOrEoln[]= { + "Not EOF or EOLN", + "Brak konca linii i brak konca pliku"}; +const char* msgNotSpace[]= { + "Not space", + "Brak spacji"}; +const char* msgNotTab[]= { + "Not tab", + "Brak znaku tabulacji"}; +const char* msgOutOfRangeInt[]= { + "Integer out of range", + "Liczba calkowita spoza zakresu"}; +const char* msgOutOfRangeReal[]= { + "Real number out of range", + "Liczba rzeczywista spoza zakresu"}; +const char* msgRealNumberLimit[]= { + "Too many digits after dot", + "Za duzo cyfr po kropce dziesietnej"}; +const char* msgBadRealNumberFormat[]= { + "Bad real number format", + "Niepoprawny format liczby rzeczywistej"}; + +// ------------------------------- Implementation ----------------------------- + +typedef unsigned long long ull; +typedef unsigned int uint; +typedef long long ll; +typedef long double ldb; + + +inline bool isDot(char x) { + return x == '.'; +} + +inline bool isEOLN(char x) { + return x == '\n'; +} + +inline bool isMinus(char x) { + return x == '-'; +} + +inline bool isSpace(char x) { + return x == ' '; +} + +inline bool isTab(char x) { + return x == '\t'; +} + +inline bool isWhitespace(char x) { + return x == ' ' || x == '\t' || x == '\n'; +} + +void endDefault(const char* msg, int line, int position) { + printf("ERROR(line: %d, position: %d): %s\n", line, position, msg); + exit(1); +} + +// ------------------------------- Random ------------------------------------- + +void Random::init() { +// Here is a reference about it: +// http://random.mat.sbg.ac.at/results/karl/server/node7.html + vector A(5, 0); + static_assert(4<=sizeof(int), "Typ int musi miescic co najmniej 4 bajty."); + static_assert(0x7FFFFFFFLL<=INT_MAX, "Wartosc 2^31-1 musi miescic sie w typie int."); + A[0] = 107374182, A[4] = 104480; + unsigned int modulo = 2147483647; // 2^31 -1 + mrg_ = new MultipleRecursiveGenerator(modulo, A); +} + +Random::Random() { + init(); +} + +Random::Random(unsigned int seed) { + init(); + setSeed(seed); +} + +Random::~Random() { + delete mrg_; +} + +void Random::setSeed(unsigned int seed) { + mrg_->setSeed(seed); +} + +static_assert(((8==CHAR_BIT) && (UCHAR_MAX==0xFF)), "Wiecej niz 8 bitow w bajcie? Nie wspierane, sorry."); +#define RAND_TYPE(type)\ + type res = 0;\ + for (size_t i = 0; i < sizeof(type)/2; ++i) {\ + res |= (((type)mrg_->next16Bits()) & (0xFFFF)) << (i * 16);\ + }\ + return res; + + +int Random::rand() { + int x = randSInt(); + if (x<0) return ~x; + return x; +} + +int Random::randSInt(const int a, const int b) { + return (rand() % (b-a+1)) + a; +} + +int Random::randSInt() { + RAND_TYPE(int); +} + +unsigned int Random::randUInt() { + RAND_TYPE(unsigned int); +} + +long long Random::randSLL() { + RAND_TYPE(long long); +} + +unsigned long long Random::randULL() { + RAND_TYPE(unsigned long long); +} + +template +void Random::randomShuffle(RandomAccessIterator first, RandomAccessIterator last) { + long int n = last - first; + for (int i = 1; i < n; ++i) { + int to = rand() % (i+1); + swap(first[to], first[i]); + } +} + +// ----------------------- MultipleRecursiveGenerator ------------------------- + +MultipleRecursiveGenerator::MultipleRecursiveGenerator( + unsigned int modulo, + const vector &A) : modulo_(modulo), A_(A) { + assert(0xFFFFUL < modulo); + n_ = A_.size(); + X_ = vector(n_, 0); + setSeed(0); +} + +void MultipleRecursiveGenerator::setSeed(unsigned int seed) { + for (int i = 0; i < n_; ++i) { + seed = (seed + 1) % modulo_; + X_[i] = seed; + } + for (int i = 0; i < n_; ++i) next16Bits(); +} + +unsigned int MultipleRecursiveGenerator::next16Bits() { + unsigned int res = 0; + static_assert(2 * sizeof(unsigned int) <= sizeof(unsigned long long), "Mozliwe przepelnienie arytmetyczne!"); + for (int i = 0; i < n_; ++i) { + res = (unsigned int)((res + (unsigned long long)A_[i] * X_[i]) % (unsigned long long)modulo_); + if (i < n_ - 1) X_[i] = X_[i+1]; + } + X_[n_ - 1] = res; + return res; +} + +// --------------------------- Reader's methods ------------------------------- + +Reader::Reader(const char* file) { + assert((input = fopen(file, "r")) != NULL); + head = tail= 0; + line = position = 1; +} + +Reader::Reader(FILE* _input) { + input = _input; + head = tail = 0; + line = position = 1; +} + +Reader::~Reader() { + assert(fclose(input) == 0); +} + +void Reader::fillBuffer() { + while ((tail + 1) % bufferSize != head) { + int v = getc(input); + if (v == EOF) break; + Buffer[tail] = (char)v; + tail = (tail + 1) % bufferSize; + } +} + +bool Reader::isEOF() { + fillBuffer(); + return head == tail; +} + +char Reader::read(bool move) { + fillBuffer(); + assert((head != tail) || (!move)); + if (head == tail) return 0; + char v = Buffer[head]; + if (move) { + if (isEOLN(v)) { + line++; + position = 1; + } else { + position++; + } + head = (head + 1) % bufferSize; + } + return v; +} + +// ---------------------------- Scanner's methods ----------------------------- + +Scanner::Scanner(const char* file, Lang _lang): lang(_lang) { + reader = new Reader(file); + end = endDefault; +} + +Scanner::Scanner(const char* file, void(*endf)(const char* msg, int line, int position), Lang _lang): lang(_lang) { + reader = new Reader(file); + end = endf; +} + +Scanner::Scanner(FILE* input, Lang _lang): lang(_lang) { + reader = new Reader(input); + end = endDefault; +} + +Scanner::Scanner(FILE* input, void(*endf)(const char* msg, int line, int position), Lang _lang): lang(_lang) { + reader = new Reader(input); + end = endf; +} + +Scanner::~Scanner() { + delete reader; +} + +void Scanner::error(const char* msg) { + int l = reader->getLine(); + int p = reader->getPosition(); + delete reader; + reader = NULL; + (*end)(msg, l, p); +} + +int Scanner::skipWhitespaces() { + int result = 0; + while (isWhitespace(reader->read())) { + reader->read(1); + result++; + } + return result; +} + + +int Scanner::skipWhitespacesUntilEOLN() { + int result = 0; + while (isWhitespace(reader->read()) && !isEOLN(reader->read())) { + reader->read(1); + result++; + } + return result; +} + + +// INTEGERS + +int Scanner::readInt(int min_value, int max_value) { + return (int)readLL(min_value, max_value); +} + +uint Scanner::readUInt(uint min_value, uint max_value) { + return (uint)readULL(min_value, max_value); +} + +inline bool lower_equal(ull a, bool sign_a, ull b, bool sign_b) { + if (sign_a != sign_b) return sign_a; + if (sign_a) return a >= b; + return a <= b; +} +inline ull spec_abs(ll x) { + if (x < 0) return (-(x + 1)) + 1; + return x; +} + +ll Scanner::readLL(ll min_value, ll max_value) { + assert(min_value <= max_value); + bool sign; + ull val; + readULL(max(spec_abs(min_value), spec_abs(max_value)), val, sign); + ll v = val; + if (!(lower_equal(spec_abs(min_value), min_value < 0, v, sign) && + lower_equal(v, sign, spec_abs(max_value), max_value < 0))) + error(msgOutOfRangeInt[lang]); + if (sign) v *= -1; + return v; +} + +ull Scanner::readULL(ull min_value, ull max_value) { + assert(min_value <= max_value); + bool sign; + ull val; + readULL(max_value, val, sign); + if (sign) error(msgOutOfRangeInt[lang]); + if (!(min_value <= val)) + error(msgOutOfRangeInt[lang]); + return val; +} + +// REAL NUMBERS + +float Scanner::readFloat(float min_value, float max_value) { + return (float)readLDouble(min_value, max_value); +} + +double Scanner::readDouble(double min_value, double max_value) { + return (double)readLDouble(min_value, max_value); +} + +long double Scanner::readLDouble(long double min_value, long double max_value) { + assert(min_value <= max_value); + bool sign; + ldb val; + readLDB(val, sign); + if (sign) val *= -1; + if (!(min_value <= val && val <= max_value)) + error(msgOutOfRangeReal[lang]); + return val; +} + +// STRINGS + +int Scanner::readString(char* s, int size) { + int x = 0; + while ( x < size - 1 && !isEOF() && !isWhitespace(reader->read())) + s[x++] = reader->read(1); + s[x]=0; + return x; +} + +int Scanner::readLine(char* s, int size) { + int x = 0; + while ( x < size - 1 && !isEOLN(reader->read()) && !isEOF()) + s[x++] = reader->read(1); + s[x] = 0; + if (isEOLN(reader->read())) reader->read(1); + return x; +} + +char Scanner::readChar() { + if (reader->isEOF()) error(msgNoChar[lang]); + return reader->read(1); +} + +// WHITESPACES + +void Scanner::readEof() { + if (!reader->isEOF()) error(msgNotEof[lang]); +} + +void Scanner::readEoln() { + if (!isEOLN(reader->read())) error(msgNotEoln[lang]); + reader->read(1); +} + +void Scanner::readEofOrEoln() { + if (isEOLN(reader->read())) { + reader->read(1); + } else if (!reader->isEOF()) { + error(msgNotEofOrEoln[lang]); + } +} + + +void Scanner::readSpace() { + if (!isSpace(reader->read())) error(msgNotSpace[lang]); + reader->read(1); +} + +void Scanner::readTab() { + if (!isTab(reader->read())) error(msgNotTab[lang]); + reader->read(1); +} + +bool Scanner::isEOF() { + return reader->isEOF(); +} + + +// PROTECTED + +void Scanner::readULL(ull limit, ull &val, bool &sign) { + sign = 0; + val = 0; + sign = isMinus(reader->read()); + if (sign) reader->read(1); + int zeros = 0; + int valDigits = 0; + while ('0' == reader->read()) { + zeros++; + valDigits++; + reader->read(1); + if (zeros > 1) error(msgLeadingZeros[lang]); + } + int limDigits = 0; + ull tmp = limit; + while (tmp) { + limDigits++; + tmp /= 10; + } + if (!limDigits) limDigits = 1; + while (isdigit(reader->read())) { + valDigits++; + if (valDigits > limDigits) error(msgOutOfRangeInt[lang]); + char x = reader->read(1); + if (valDigits == limDigits) { + if (limit / 10 < val) error(msgOutOfRangeInt[lang]); + if (limit / 10 == val && limit % 10 < (ull)(x - '0')) error(msgOutOfRangeInt[lang]); + } + val = val * 10 + x - '0'; + } + if (val > 0 && zeros) error(msgLeadingZeros[lang]); + if (sign && zeros) error(msgMinusZero[lang]); + if (!valDigits) error(msgNoNumber[lang]); +} + +void Scanner::readLDB(ldb &val, bool &sign) { + sign = 0; + val = 0; + sign = isMinus(reader->read()); + if (sign) reader->read(1); + int zeros = 0; + int valDigits = 0; + while ('0' == reader->read()) { + zeros++; + valDigits++; + reader->read(1); + if (zeros > 1) error(msgLeadingZeros[lang]); + } + if (zeros && isdigit(reader->read())) error(msgLeadingZeros[lang]); + while (isdigit(reader->read())) { + valDigits++; + char x = reader->read(1); + val = val * 10.0 + x - '0'; + } + if (!valDigits) error(msgNoNumber[lang]); + if (isDot(reader->read())) { + reader->read(1); + ldb dec = 1; + int dotDigits = 0; + while (isdigit(reader->read())) { + dotDigits++; + if (dotDigits > realNumbersLimit) error(msgRealNumberLimit[lang]); + char x = reader->read(1); + dec /= 10.0; + val += dec * (x - '0'); + } + if (!dotDigits) error(msgBadRealNumberFormat[lang]); + } +} + +} // namespace oi + +#endif // OI_LIB_OI_H_ diff --git a/src/sinol_make/commands/inwer/__init__.py b/src/sinol_make/commands/inwer/__init__.py index 2f942adf..edbeb588 100644 --- a/src/sinol_make/commands/inwer/__init__.py +++ b/src/sinol_make/commands/inwer/__init__.py @@ -84,7 +84,7 @@ def verify_and_print_table(self) -> Dict[str, TestResult]: :return: dictionary of TestResult objects """ results = {} - sorted_tests = sorted(self.tests, key=lambda x: x[0]) + sorted_tests = sorted(self.tests, key=lambda test: package_util.get_group(test, self.task_id)) executions: List[InwerExecution] = [] for test in sorted_tests: results[test] = TestResult(test, self.task_id) From 7a56f1f3ec5f3492dce6ff687a04826d728b83aa Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 22:32:50 +0200 Subject: [PATCH 2/5] Change `--solutions` and `--tests` flags behaviour (#126) * Change behaviour of --solutions flag * Add tests * Move function for getting solutions to `package_util`, change getting tests * Add tests * Refactor --- src/sinol_make/commands/run/__init__.py | 42 ++------------ src/sinol_make/helpers/package_util.py | 77 +++++++++++++++++++++++-- tests/commands/run/test_integration.py | 29 +++++++++- tests/commands/run/test_unit.py | 19 +----- tests/helpers/test_package_util.py | 65 +++++++++++++++++++++ 5 files changed, 171 insertions(+), 61 deletions(-) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index b82b4d82..f8a08475 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -282,22 +282,6 @@ def get_group(self, test_path): return int("".join(filter(str.isdigit, package_util.extract_test_id(test_path, self.ID)))) - def get_executable_key(self, executable): - name = package_util.get_file_name(executable) - value = [0, 0] - if name[3] == 's': - value[0] = 1 - suffix = name.split(".")[0][4:] - elif name[3] == 'b': - value[0] = 2 - suffix = name.split(".")[0][4:] - else: - suffix = name.split(".")[0][3:] - if suffix != "": - value[1] = int(suffix) - return tuple(value) - - def get_solution_from_exe(self, executable): file = os.path.splitext(executable)[0] for ext in self.SOURCE_EXTENSIONS: @@ -305,24 +289,8 @@ def get_solution_from_exe(self, executable): return file + ext util.exit_with_error("Source file not found for executable %s" % executable) - - def get_solutions(self, args_solutions): - if args_solutions is None: - solutions = [solution for solution in os.listdir("prog/") - if self.SOLUTIONS_RE.match(solution)] - return sorted(solutions, key=self.get_executable_key) - else: - solutions = [] - for solution in args_solutions: - if not os.path.isfile(solution): - util.exit_with_error("Solution %s does not exist" % solution) - if self.SOLUTIONS_RE.match(os.path.basename(solution)) is not None: - solutions.append(os.path.basename(solution)) - return sorted(solutions, key=self.get_executable_key) - - def get_executables(self, args_solutions): - return [package_util.get_executable(solution) for solution in self.get_solutions(args_solutions)] + return [package_util.get_executable(solution) for solution in package_util.get_solutions(self.ID, args_solutions)] def get_possible_score(self, groups): @@ -659,7 +627,7 @@ def run_solutions(self, compiled_commands, names, solutions): for test in self.tests: all_results[name][self.get_group(test)][test] = ExecutionResult(Status.CE) print() - executions.sort(key = lambda x: (self.get_executable_key(x[1]), x[2])) + executions.sort(key = lambda x: (package_util.get_executable_key(x[1]), x[2])) program_groups_scores = collections.defaultdict(dict) print_data = PrintData(0) @@ -1000,11 +968,11 @@ def set_group_result(solution, group, result): def set_constants(self): self.ID = package_util.get_task_id() self.SOURCE_EXTENSIONS = ['.c', '.cpp', '.py', '.java'] - self.SOLUTIONS_RE = re.compile(r"^%s[bs]?[0-9]*\.(cpp|cc|java|py|pas)$" % self.ID) + self.SOLUTIONS_RE = package_util.get_solutions_re(self.ID) def validate_arguments(self, args): - compilers = compiler.verify_compilers(args, self.get_solutions(None)) + compilers = compiler.verify_compilers(args, package_util.get_solutions(self.ID, None)) def use_oiejq(): timetool_path = None @@ -1205,7 +1173,7 @@ def run(self, args): self.check_are_any_tests_to_run() self.set_scores() self.failed_compilations = [] - solutions = self.get_solutions(self.args.solutions) + solutions = package_util.get_solutions(self.ID, self.args.solutions) util.change_stack_size_to_unlimited() for solution in solutions: diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index 8ace8c8a..ac08f953 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -45,6 +45,48 @@ def get_test_key(test, task_id): return get_group(test, task_id), test +def get_solutions_re(task_id): + return re.compile(r"^%s[bs]?[0-9]*\.(cpp|cc|java|py|pas)$" % task_id) + + +def get_executable_key(executable): + name = get_file_name(executable) + value = [0, 0] + if name[3] == 's': + value[0] = 1 + suffix = name.split(".")[0][4:] + elif name[3] == 'b': + value[0] = 2 + suffix = name.split(".")[0][4:] + else: + suffix = name.split(".")[0][3:] + if suffix != "": + value[1] = int(suffix) + return tuple(value) + + +def get_files_matching(patterns: List[str], directory: str) -> List[str]: + """ + Returns list of files matching given patterns. + If pattern is absolute path, it is returned as is. + If pattern is relative path, it is searched in current directory and in directory specified as argument. + :param patterns: List of patterns to match. + :param directory: Directory to search in. + :return: List of files matching given patterns. + """ + files_matching = set() + for solution in patterns: + if os.path.isabs(solution): + files_matching.add(solution) + else: + # If solution already has `/` prefix: + files_matching.update(glob.glob(os.path.join(os.getcwd(), solution))) + # If solution does not have `/` prefix: + files_matching.update(glob.glob(os.path.join(os.getcwd(), directory, solution))) + + return list(files_matching) + + def get_tests(task_id: str, arg_tests: Union[List[str], None] = None) -> List[str]: """ Returns list of tests to run. @@ -57,11 +99,36 @@ def get_tests(task_id: str, arg_tests: Union[List[str], None] = None) -> List[st if test[-3:] == ".in"] return sorted(all_tests, key=lambda test: get_test_key(test, task_id)) else: - existing_tests = set() - for test in arg_tests: - if os.path.exists(test): - existing_tests.add(test) - return sorted(list(existing_tests), key=lambda test: get_test_key(test, task_id)) + existing_tests = [] + for test in get_files_matching(arg_tests, "in"): + if not os.path.isfile(test): + util.exit_with_error("Test %s does not exist" % test) + if os.path.splitext(test)[1] == ".in": + existing_tests.append(os.path.join("in", os.path.basename(test))) + return sorted(existing_tests, key=lambda test: get_test_key(test, task_id)) + + +def get_solutions(task_id: str, args_solutions: Union[List[str], None] = None) -> List[str]: + """ + Returns list of solutions to run. + :param task_id: Task id. + :param args_solutions: Solutions specified in command line arguments. If None, all solutions are returned. + :return: List of solutions to run. + """ + solutions_re = get_solutions_re(task_id) + if args_solutions is None: + solutions = [solution for solution in os.listdir("prog/") + if solutions_re.match(solution)] + return sorted(solutions, key=get_executable_key) + else: + solutions = [] + for solution in get_files_matching(args_solutions, "prog"): + if not os.path.isfile(solution): + util.exit_with_error("Solution %s does not exist" % solution) + if solutions_re.match(os.path.basename(solution)) is not None: + solutions.append(os.path.basename(solution)) + + return sorted(solutions, key=get_executable_key) def get_file_name(file_path): diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index c9837a8c..9e28d86e 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -147,7 +147,7 @@ def test_flag_tests(create_package, time_tool): except SystemExit: pass - assert command.tests == [test] + assert command.tests == [os.path.join("in", os.path.basename(test))] @pytest.mark.parametrize("create_package", [get_checker_package_path()], indirect=True) @@ -233,6 +233,33 @@ def test_flag_solutions(capsys, create_package, time_tool): assert os.path.basename(solutions[1]) not in out +@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), + get_checker_package_path()], indirect=True) +def test_flag_solutions_multiple(capsys, create_package, time_tool): + """ + Test flag --solutions with multiple solutions. + """ + package_path = create_package + create_ins_outs(package_path) + + task_id = package_util.get_task_id() + solutions = [ + os.path.basename(file) + for file in package_util.get_files_matching_pattern(task_id, f'{task_id}?.*') + ] + parser = configure_parsers() + args = parser.parse_args(["run", "--solutions", solutions[0], os.path.join("prog", solutions[1]), + "--time-tool", time_tool]) + command = Command() + command.run(args) + + out = capsys.readouterr().out + + assert os.path.basename(solutions[0]) in out + assert os.path.basename(solutions[1]) in out + assert os.path.basename(solutions[2]) not in out + + @pytest.mark.parametrize("create_package", [get_weak_compilation_flags_package_path()], indirect=True) def test_weak_compilation_flags(create_package): """ diff --git a/tests/commands/run/test_unit.py b/tests/commands/run/test_unit.py index eb1802a2..683aa553 100644 --- a/tests/commands/run/test_unit.py +++ b/tests/commands/run/test_unit.py @@ -15,27 +15,10 @@ def test_get_output_file(): assert command.get_output_file("in/abc1a.in") == "out/abc1a.out" -def test_get_solutions(): - os.chdir(get_simple_package_path()) - command = get_command() - - solutions = command.get_solutions(None) - assert solutions == ["abc.cpp", "abc1.cpp", "abc2.cpp", "abc3.cpp", "abc4.cpp"] - solutions = command.get_solutions(["prog/abc.cpp"]) - assert solutions == ["abc.cpp"] - assert "abc1.cpp" not in solutions - - -def test_get_executable_key(): - os.chdir(get_simple_package_path()) - command = get_command() - assert command.get_executable_key("abc1.cpp.e") == (0, 1) - - def test_compile_solutions(create_package): package_path = create_package command = get_command(package_path) - solutions = command.get_solutions(None) + solutions = package_util.get_solutions("abc", None) result = command.compile_solutions(solutions) assert result == [True for _ in solutions] diff --git a/tests/helpers/test_package_util.py b/tests/helpers/test_package_util.py index de5f294a..44ca3ce4 100644 --- a/tests/helpers/test_package_util.py +++ b/tests/helpers/test_package_util.py @@ -36,6 +36,30 @@ def test_get_tests(create_package): tests = package_util.get_tests("abc", None) assert tests == ["in/abc1a.in", "in/abc2a.in", "in/abc3a.in", "in/abc4a.in"] + with tempfile.TemporaryDirectory() as tmpdir: + def create_file(name): + with open(os.path.join(tmpdir, "in", name), "w") as f: + f.write("") + + os.chdir(tmpdir) + os.mkdir("in") + create_file("abc0.in") + create_file("abc0a.in") + create_file("abc1ocen.in") + create_file("abc2ocen.in") + create_file("abc1a.in") + create_file("abc1b.in") + create_file("abc2a.in") + + assert set(package_util.get_tests("abc", None)) == \ + {"in/abc0.in", "in/abc0a.in", "in/abc1a.in", "in/abc1b.in", "in/abc1ocen.in", "in/abc2a.in", "in/abc2ocen.in"} + assert package_util.get_tests("abc", ["in/abc1a.in"]) == ["in/abc1a.in"] + assert package_util.get_tests("abc", ["in/abc??.in"]) == \ + ["in/abc0a.in", "in/abc1a.in", "in/abc1b.in", "in/abc2a.in"] + assert package_util.get_tests("abc", ["abc1a.in"]) == ["in/abc1a.in"] + assert package_util.get_tests("abc", ["abc?ocen.in", "abc0.in"]) == ["in/abc0.in", "in/abc1ocen.in", "in/abc2ocen.in"] + assert package_util.get_tests("abc", [os.path.join(tmpdir, "in", "abc1a.in")]) == ["in/abc1a.in"] + def test_extract_file_name(): assert package_util.get_file_name("in/abc1a.in") == "abc1a.in" @@ -203,3 +227,44 @@ def test_validate_files(create_package, capsys): package_util.validate_test_names(task_id) out = capsys.readouterr().out assert "def1a.out" in out + + +def test_get_executable_key(): + os.chdir(get_simple_package_path()) + assert package_util.get_executable_key("abc1.cpp.e") == (0, 1) + + +def test_get_solutions(): + os.chdir(get_simple_package_path()) + + solutions = package_util.get_solutions("abc", None) + assert solutions == ["abc.cpp", "abc1.cpp", "abc2.cpp", "abc3.cpp", "abc4.cpp"] + solutions = package_util.get_solutions("abc", ["prog/abc.cpp"]) + assert solutions == ["abc.cpp"] + assert "abc1.cpp" not in solutions + + with tempfile.TemporaryDirectory() as tmpdir: + def create_file(name): + with open(os.path.join(tmpdir, "prog", name), "w") as f: + f.write("") + + os.chdir(tmpdir) + os.mkdir("prog") + + create_file("abc.cpp") + create_file("abc1.cpp") + create_file("abc2.cpp") + create_file("abcs1.cpp") + create_file("abcs2.cpp") + + assert package_util.get_solutions("abc", None) == ["abc.cpp", "abc1.cpp", "abc2.cpp", "abcs1.cpp", "abcs2.cpp"] + assert package_util.get_solutions("abc", ["prog/abc.cpp"]) == ["abc.cpp"] + assert package_util.get_solutions("abc", ["abc.cpp"]) == ["abc.cpp"] + assert package_util.get_solutions("abc", [os.path.join(tmpdir, "prog", "abc.cpp")]) == ["abc.cpp"] + assert package_util.get_solutions("abc", ["prog/abc?.cpp"]) == ["abc1.cpp", "abc2.cpp"] + assert package_util.get_solutions("abc", ["abc?.cpp"]) == ["abc1.cpp", "abc2.cpp"] + assert package_util.get_solutions("abc", ["prog/abc*.cpp"]) == ["abc.cpp", "abc1.cpp", "abc2.cpp", "abcs1.cpp", "abcs2.cpp"] + assert package_util.get_solutions("abc", ["abc*.cpp"]) == ["abc.cpp", "abc1.cpp", "abc2.cpp", "abcs1.cpp", "abcs2.cpp"] + assert package_util.get_solutions("abc", ["prog/abc.cpp", "abc1.cpp"]) == ["abc.cpp", "abc1.cpp"] + assert package_util.get_solutions("abc", ["prog/abc.cpp", "abc?.cpp"]) == ["abc.cpp", "abc1.cpp", "abc2.cpp"] + assert package_util.get_solutions("abc", ["abc.cpp", "abc2.cpp", "abcs2.cpp"]) == ["abc.cpp", "abc2.cpp", "abcs2.cpp"] From 11900e810ef99b72ce17fa83f5c6a83e765645bd Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 24 Sep 2023 11:01:56 +0200 Subject: [PATCH 3/5] Cache run results (#127) * Refactor compilation caching * Refactor, add caching of test runs * Fix test * Change conftest * Add types, remove result cache after new checker compilation * Add tests * Refactor * Print compile log on failed compilation in conftest * Workflow debug * Workflow debug * Better error handling * Remove cache when extra compilation files change * Add tests * Add warning about corrupted cache files * Throw error when parsing incorrect Status * Refactor * Fix tests --- src/sinol_make/commands/gen/__init__.py | 5 +- src/sinol_make/commands/inwer/__init__.py | 2 +- src/sinol_make/commands/run/__init__.py | 71 +++++++---- src/sinol_make/commands/run/structs.py | 77 ------------ src/sinol_make/contest_types/default.py | 2 +- src/sinol_make/contest_types/oi.py | 2 +- src/sinol_make/helpers/cache.py | 102 ++++++++++++++++ src/sinol_make/helpers/compile.py | 51 +------- src/sinol_make/helpers/package_util.py | 6 +- src/sinol_make/structs/cache_structs.py | 79 ++++++++++++ .../gen/structs.py => structs/gen_structs.py} | 0 .../structs.py => structs/inwer_structs.py} | 0 src/sinol_make/structs/run_structs.py | 29 +++++ src/sinol_make/structs/status_structs.py | 89 ++++++++++++++ tests/commands/gen/test_unit.py | 3 +- tests/commands/run/test_integration.py | 112 ++++++++++++++++++ tests/commands/run/test_unit.py | 4 +- tests/conftest.py | 32 ++++- tests/contest_types/test_default.py | 2 +- tests/contest_types/test_oi.py | 2 +- tests/helpers/test_cache.py | 81 +++++++++++-- tests/helpers/test_compile.py | 10 +- tests/util.py | 1 - 23 files changed, 590 insertions(+), 172 deletions(-) delete mode 100644 src/sinol_make/commands/run/structs.py create mode 100644 src/sinol_make/helpers/cache.py create mode 100644 src/sinol_make/structs/cache_structs.py rename src/sinol_make/{commands/gen/structs.py => structs/gen_structs.py} (100%) rename src/sinol_make/{commands/inwer/structs.py => structs/inwer_structs.py} (100%) create mode 100644 src/sinol_make/structs/run_structs.py diff --git a/src/sinol_make/commands/gen/__init__.py b/src/sinol_make/commands/gen/__init__.py index b02ab0b7..9a690116 100644 --- a/src/sinol_make/commands/gen/__init__.py +++ b/src/sinol_make/commands/gen/__init__.py @@ -1,15 +1,14 @@ import argparse import glob import os -import hashlib import yaml import multiprocessing as mp from sinol_make import util from sinol_make.commands.gen import gen_util -from sinol_make.commands.gen.structs import OutputGenerationArguments -from sinol_make.helpers import parsers, package_util, compile +from sinol_make.structs.gen_structs import OutputGenerationArguments +from sinol_make.helpers import parsers, package_util from sinol_make.interfaces.BaseCommand import BaseCommand diff --git a/src/sinol_make/commands/inwer/__init__.py b/src/sinol_make/commands/inwer/__init__.py index edbeb588..58bf3e61 100644 --- a/src/sinol_make/commands/inwer/__init__.py +++ b/src/sinol_make/commands/inwer/__init__.py @@ -8,7 +8,7 @@ from typing import Dict, List from sinol_make import util -from sinol_make.commands.inwer.structs import TestResult, InwerExecution, VerificationResult, TableData +from sinol_make.structs.inwer_structs import TestResult, InwerExecution, VerificationResult, TableData from sinol_make.helpers import package_util, compile, printer, paths from sinol_make.helpers.parsers import add_compilation_arguments from sinol_make.interfaces.BaseCommand import BaseCommand diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index f8a08475..5f305a5a 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -11,13 +11,13 @@ from typing import Dict from sinol_make import contest_types, oiejq -from sinol_make.commands.run.structs import ExecutionResult, ResultChange, ValidationResult, ExecutionData, \ - PointsChange, PrintData +from sinol_make.structs.run_structs import ExecutionData, PrintData +from sinol_make.structs.cache_structs import CacheTest, CacheFile from sinol_make.helpers.parsers import add_compilation_arguments from sinol_make.interfaces.BaseCommand import BaseCommand from sinol_make.interfaces.Errors import CompilationError, CheckerOutputException, UnknownContestType -from sinol_make.helpers import compile, compiler, package_util, printer, paths -from sinol_make.structs.status_structs import Status +from sinol_make.helpers import compile, compiler, package_util, printer, paths, cache +from sinol_make.structs.status_structs import Status, ResultChange, PointsChange, ValidationResult, ExecutionResult import sinol_make.util as util import yaml, os, collections, sys, re, math, dictdiffer import multiprocessing as mp @@ -308,17 +308,17 @@ def get_groups(self, tests): return sorted(list(set([self.get_group(test) for test in tests]))) - def compile_solutions(self, solutions): + def compile_solutions(self, solutions, is_checker=False): os.makedirs(paths.get_compilation_log_path(), exist_ok=True) os.makedirs(paths.get_executables_path(), exist_ok=True) print("Compiling %d solutions..." % len(solutions)) - args = [(solution, True) for solution in solutions] + args = [(solution, True, is_checker) for solution in solutions] with mp.Pool(self.cpus) as pool: compilation_results = pool.starmap(self.compile, args) return compilation_results - def compile(self, solution, use_extras = False): + def compile(self, solution, use_extras = False, is_checker = False): compile_log_file = paths.get_compilation_log_path("%s.compile_log" % package_util.get_file_name(solution)) source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution)) output = paths.get_executables_path(package_util.get_executable(solution)) @@ -339,7 +339,7 @@ def compile(self, solution, use_extras = False): try: with open(compile_log_file, "w") as compile_log: compile.compile(source_file, output, self.compilers, compile_log, self.args.weak_compilation_flags, - extra_compilation_args, extra_compilation_files) + extra_compilation_args, extra_compilation_files, is_checker=is_checker) print(util.info("Compilation of file %s was successful." % package_util.get_file_name(solution))) return True @@ -400,7 +400,6 @@ def check_output(self, name, input_file, output_file_path, output, answer_file_p output_file.write("\n".join(output) + "\n") return self.check_output_checker(name, input_file, output_file_path, answer_file_path) - def execute_oiejq(self, command, name, result_file_path, input_file_path, output_file_path, answer_file_path, time_limit, memory_limit, hard_time_limit): env = os.environ.copy() @@ -611,17 +610,29 @@ def run_solutions(self, compiled_commands, names, solutions): """ executions = [] + all_cache_files: Dict[str, CacheFile] = {} all_results = collections.defaultdict( lambda: collections.defaultdict(lambda: collections.defaultdict(map))) + for (name, executable, result) in compiled_commands: lang = package_util.get_file_lang(name) + solution_cache = cache.get_cache_file(os.path.join(os.getcwd(), "prog", name)) + all_cache_files[name] = solution_cache + if result: for test in self.tests: - executions.append((name, executable, test, - package_util.get_time_limit(test, self.config, lang, self.ID, self.args), - package_util.get_memory_limit(test, self.config, lang, self.ID, self.args), - self.timetool_path)) - all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING) + test_time_limit = package_util.get_time_limit(test, self.config, lang, self.ID, self.args) + test_memory_limit = package_util.get_memory_limit(test, self.config, lang, self.ID, self.args) + + test_result: CacheTest = solution_cache.tests.get(self.test_md5sums[os.path.basename(test)], None) + if test_result is not None and test_result.time_limit == test_time_limit and \ + test_result.memory_limit == test_memory_limit and \ + test_result.time_tool == self.timetool_name: + all_results[name][self.get_group(test)][test] = test_result.result + else: + executions.append((name, executable, test, test_time_limit, test_memory_limit, + self.timetool_path)) + all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING) os.makedirs(paths.get_executions_path(name), exist_ok=True) else: for test in self.tests: @@ -651,6 +662,17 @@ def run_solutions(self, compiled_commands, names, solutions): result.Points = contest_points all_results[name][self.get_group(test)][test] = result print_data.i = i + + # We store the result in dictionary to write it to cache files later. + lang = package_util.get_file_lang(name) + test_time_limit = package_util.get_time_limit(test, self.config, lang, self.ID, self.args) + test_memory_limit = package_util.get_memory_limit(test, self.config, lang, self.ID, self.args) + all_cache_files[name].tests[self.test_md5sums[os.path.basename(test)]] = CacheTest( + time_limit=test_time_limit, + memory_limit=test_memory_limit, + time_tool=self.timetool_name, + result=result + ) pool.terminate() except KeyboardInterrupt: keyboard_interrupt = True @@ -664,6 +686,10 @@ def run_solutions(self, compiled_commands, names, solutions): names, executions, self.groups, self.scores, self.tests, self.possible_score, self.cpus, self.args.hide_memory, self.config, self.contest, self.args)[0])) + # Write cache files. + for solution, cache_data in all_cache_files.items(): + cache_data.save(os.path.join(os.getcwd(), "prog", solution)) + if keyboard_interrupt: util.exit_with_error("Stopped due to keyboard interrupt.") @@ -1127,6 +1153,14 @@ def check_errors(self, results: Dict[str, Dict[str, Dict[str, ExecutionResult]]] if error_msg != "": util.exit_with_error(error_msg) + def compile_checker(self): + checker_basename = os.path.basename(self.checker) + self.checker_executable = paths.get_executables_path(checker_basename + ".e") + + checker_compilation = self.compile_solutions([self.checker], is_checker=True) + if not checker_compilation[0]: + util.exit_with_error('Checker compilation failed.') + def run(self, args): util.exit_if_not_package() @@ -1152,17 +1186,13 @@ def run(self, args): title = self.config["title"] print("Task: %s (tag: %s)" % (title, self.ID)) self.cpus = args.cpus or mp.cpu_count() + cache.save_to_cache_extra_compilation_files(self.config.get("extra_compilation_files", []), self.ID) checker = package_util.get_files_matching_pattern(self.ID, f'{self.ID}chk.*') if len(checker) != 0: print(util.info("Checker found: %s" % os.path.basename(checker[0]))) self.checker = checker[0] - checker_basename = os.path.basename(self.checker) - self.checker_executable = paths.get_executables_path(checker_basename + ".e") - - checker_compilation = self.compile_solutions([self.checker]) - if not checker_compilation[0]: - util.exit_with_error('Checker compilation failed.') + self.compile_checker() else: self.checker = None @@ -1170,6 +1200,7 @@ def run(self, args): self.has_lib = len(lib) != 0 self.tests = package_util.get_tests(self.ID, self.args.tests) + self.test_md5sums = {os.path.basename(test): util.get_file_md5(test) for test in self.tests} self.check_are_any_tests_to_run() self.set_scores() self.failed_compilations = [] diff --git a/src/sinol_make/commands/run/structs.py b/src/sinol_make/commands/run/structs.py deleted file mode 100644 index 65426cbb..00000000 --- a/src/sinol_make/commands/run/structs.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import List -from dataclasses import dataclass - -from sinol_make.structs.status_structs import Status - - -@dataclass -class ResultChange: - solution: str - group: int - old_result: Status - result: Status - -@dataclass -class PointsChange: - solution: str - group: int - old_points: int - new_points: int - -@dataclass -class ValidationResult: - added_solutions: set - removed_solutions: set - added_groups: set - removed_groups: set - changes: List[ResultChange] - expected_scores: dict - new_expected_scores: dict - -@dataclass -class ExecutionResult: - # Result status of execution. Possible values - # can be found in `Status` enum definition. - Status: Status - # Time in milliseconds - Time: float - # Memory in KB - Memory: int - # Points for this test - Points: int - # Error message - Error: str - - def __init__(self, status=None, Time=None, Memory=None, Points=0, Error=None): - self.Status = status - self.Time = Time - self.Memory = Memory - self.Points = Points - self.Error = Error - -@dataclass -class ExecutionData: - """ - Represents data for execution of a solution on a specified test. - """ - # Name of the solution - name: str - # Filename of the executable - executable: str - # Filename of the test - test: str - # Time limit for this test in milliseconds - time_limit: int - # Memory limit in KB - memory_limit: int - # Path to the timetool executable - timetool_path: str - - -@dataclass -class PrintData: - """ - Represents data for printing results of execution. - """ - - i: int diff --git a/src/sinol_make/contest_types/default.py b/src/sinol_make/contest_types/default.py index bc0b0e62..727570a0 100644 --- a/src/sinol_make/contest_types/default.py +++ b/src/sinol_make/contest_types/default.py @@ -1,7 +1,7 @@ from math import ceil from typing import List -from sinol_make.commands.run.structs import ExecutionResult +from sinol_make.structs.status_structs import ExecutionResult class DefaultContest: diff --git a/src/sinol_make/contest_types/oi.py b/src/sinol_make/contest_types/oi.py index 8136dd8e..a85a0f79 100644 --- a/src/sinol_make/contest_types/oi.py +++ b/src/sinol_make/contest_types/oi.py @@ -1,4 +1,4 @@ -from sinol_make.commands.run.structs import ExecutionResult +from sinol_make.structs.status_structs import ExecutionResult from sinol_make.contest_types.default import DefaultContest diff --git a/src/sinol_make/helpers/cache.py b/src/sinol_make/helpers/cache.py new file mode 100644 index 00000000..5d9a6993 --- /dev/null +++ b/src/sinol_make/helpers/cache.py @@ -0,0 +1,102 @@ +import os +import yaml +from typing import Union + +from sinol_make import util +from sinol_make.structs.cache_structs import CacheFile +from sinol_make.helpers import paths, package_util + + +def get_cache_file(solution_path: str) -> CacheFile: + """ + Returns content of cache file for given solution + :param solution_path: Path to solution + :return: Content of cache file + """ + os.makedirs(paths.get_cache_path("md5sums"), exist_ok=True) + cache_file_path = paths.get_cache_path("md5sums", os.path.basename(solution_path)) + try: + with open(cache_file_path, 'r') as cache_file: + data = yaml.load(cache_file, Loader=yaml.FullLoader) + if not isinstance(data, dict): + print(util.warning(f"Cache file for program {os.path.basename(solution_path)} is corrupted.")) + os.unlink(cache_file_path) + return CacheFile() + try: + return CacheFile.from_dict(data) + except ValueError as exc: + print(util.error(f"An error occured while parsing cache file for solution {os.path.basename(solution_path)}.")) + util.exit_with_error(str(exc)) + except FileNotFoundError: + return CacheFile() + except (yaml.YAMLError, TypeError): + print(util.warning(f"Cache file for program {os.path.basename(solution_path)} is corrupted.")) + os.unlink(cache_file_path) + return CacheFile() + + +def check_compiled(file_path: str) -> Union[str, None]: + """ + Check if a file is compiled + :param file_path: Path to the file + :return: executable path if compiled, None otherwise + """ + md5sum = util.get_file_md5(file_path) + try: + info = get_cache_file(file_path) + if info.md5sum == md5sum: + exe_path = info.executable_path + if os.path.exists(exe_path): + return exe_path + return None + except FileNotFoundError: + return None + + +def save_compiled(file_path: str, exe_path: str, is_checker: bool = False): + """ + Save the compiled executable path to cache in `.cache/md5sums/`, + which contains the md5sum of the file and the path to the executable. + :param file_path: Path to the file + :param exe_path: Path to the compiled executable + :param is_checker: Whether the compiled file is a checker. If True, all cached tests are removed. + """ + info = get_cache_file(file_path) + info.executable_path = exe_path + info.md5sum = util.get_file_md5(file_path) + info.save(file_path) + + if is_checker: + for solution in os.listdir(paths.get_cache_path('md5sums')): + info = get_cache_file(solution) + info.tests = {} + info.save(solution) + + +def save_to_cache_extra_compilation_files(extra_compilation_files, task_id): + """ + Checks if extra compilation files have changed and saves them to cache. + If they have, removes all cached solutions that use them. + :param extra_compilation_files: List of extra compilation files + :param task_id: Task id + """ + solutions_re = package_util.get_solutions_re(task_id) + for file in extra_compilation_files: + file_path = os.path.join(os.getcwd(), "prog", file) + if not os.path.exists(file_path): + continue + md5sum = util.get_file_md5(file_path) + lang = package_util.get_file_lang(file) + if lang == 'h': + lang = 'cpp' + info = get_cache_file(file_path) + + if info.md5sum != md5sum: + for solution in os.listdir(paths.get_cache_path('md5sums')): + # Remove only files in the same language and matching the solution regex + if package_util.get_file_lang(solution) == lang and \ + solutions_re.match(solution) is not None: + os.unlink(paths.get_cache_path('md5sums', solution)) + + info.md5sum = md5sum + info.save(file_path) diff --git a/src/sinol_make/helpers/compile.py b/src/sinol_make/helpers/compile.py index b8defdeb..4baea5bf 100644 --- a/src/sinol_make/helpers/compile.py +++ b/src/sinol_make/helpers/compile.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union, Dict +from typing import Tuple, Union import os import sys import shutil @@ -7,56 +7,14 @@ import yaml import sinol_make.helpers.compiler as compiler -from sinol_make import util from sinol_make.helpers import paths +from sinol_make.helpers.cache import check_compiled, save_compiled from sinol_make.interfaces.Errors import CompilationError from sinol_make.structs.compiler_structs import Compilers -def create_compilation_cache(): - os.makedirs(paths.get_cache_path("md5sums"), exist_ok=True) - - -def check_compiled(file_path: str): - """ - Check if a file is compiled - :param file_path: Path to the file - :return: executable path if compiled, None otherwise - """ - create_compilation_cache() - md5sum = util.get_file_md5(file_path) - try: - info_file_path = paths.get_cache_path("md5sums", os.path.basename(file_path)) - with open(info_file_path, 'r') as info_file: - info = yaml.load(info_file, Loader=yaml.FullLoader) - if info.get("md5sum", "") == md5sum: - exe_path = info.get("executable_path", "") - if os.path.exists(exe_path): - return exe_path - return None - except FileNotFoundError: - return None - - -def save_compiled(file_path: str, exe_path: str): - """ - Save the compiled executable path to cache in `.cache/md5sums/`, - which contains the md5sum of the file and the path to the executable. - :param file_path: Path to the file - :param exe_path: Path to the compiled executable - """ - create_compilation_cache() - info_file_path = paths.get_cache_path("md5sums", os.path.basename(file_path)) - info = { - "md5sum": util.get_file_md5(file_path), - "executable_path": exe_path - } - with open(info_file_path, 'w') as info_file: - yaml.dump(info, info_file) - - def compile(program, output, compilers: Compilers = None, compile_log = None, weak_compilation_flags = False, - extra_compilation_args = None, extra_compilation_files = None): + extra_compilation_args = None, extra_compilation_files = None, is_checker = False): """ Compile a program. :param program: Path to the program to compile @@ -66,6 +24,7 @@ def compile(program, output, compilers: Compilers = None, compile_log = None, we :param weak_compilation_flags: If True, disable all warnings :param extra_compilation_args: Extra compilation arguments :param extra_compilation_files: Extra compilation files + :param is_checker: Set to True if compiling a checker. This will remove all cached test results. """ if extra_compilation_args is None: extra_compilation_args = [] @@ -134,7 +93,7 @@ def compile(program, output, compilers: Compilers = None, compile_log = None, we if process.returncode != 0: raise CompilationError('Compilation failed') else: - save_compiled(program, output) + save_compiled(program, output, is_checker) return True diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index ac08f953..3bc310a6 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -45,7 +45,11 @@ def get_test_key(test, task_id): return get_group(test, task_id), test -def get_solutions_re(task_id): +def get_solutions_re(task_id: str) -> re.Pattern: + """ + Returns regex pattern matching all solutions for given task. + :param task_id: Task id. + """ return re.compile(r"^%s[bs]?[0-9]*\.(cpp|cc|java|py|pas)$" % task_id) diff --git a/src/sinol_make/structs/cache_structs.py b/src/sinol_make/structs/cache_structs.py new file mode 100644 index 00000000..5d1f3933 --- /dev/null +++ b/src/sinol_make/structs/cache_structs.py @@ -0,0 +1,79 @@ +import os +from dataclasses import dataclass +from typing import Dict + +import yaml + +from sinol_make.helpers import paths + +from sinol_make.structs.status_structs import ExecutionResult + + +@dataclass +class CacheTest: + # Time limit when this solution was cached + time_limit: int + # Memory limit when this solution was cached + memory_limit: int + # Time tool used when this solution was cached + time_tool: str + # Cached result + result: ExecutionResult + + def __init__(self, time_limit=0, memory_limit=0, time_tool="", result=None): + if result is None: + result = ExecutionResult() + self.time_limit = time_limit + self.memory_limit = memory_limit + self.time_tool = time_tool + self.result = result + + def to_dict(self) -> Dict: + return { + "time_limit": self.time_limit, + "memory_limit": self.memory_limit, + "time_tool": self.time_tool, + "result": self.result.to_dict() + } + + +@dataclass +class CacheFile: + # md5sum of solution + md5sum: str + # Path to the executable + executable_path: str + # Test results + tests: Dict[str, CacheTest] + + def __init__(self, md5sum="", executable_path="", tests=None): + if tests is None: + tests = {} + self.md5sum = md5sum + self.executable_path = executable_path + self.tests = tests + + def to_dict(self) -> Dict: + return { + "md5sum": self.md5sum, + "executable_path": self.executable_path, + "tests": {k: v.to_dict() for k, v in self.tests.items()} + } + + @staticmethod + def from_dict(dict) -> 'CacheFile': + return CacheFile( + md5sum=dict.get("md5sum", ""), + executable_path=dict.get("executable_path", ""), + tests={k: CacheTest( + time_limit=v["time_limit"], + memory_limit=v["memory_limit"], + time_tool=v["time_tool"], + result=ExecutionResult.from_dict(v["result"]) + ) for k, v in dict.get("tests", {}).items()} + ) + + def save(self, solution_path: str): + os.makedirs(paths.get_cache_path("md5sums"), exist_ok=True) + with open(paths.get_cache_path("md5sums", os.path.basename(solution_path)), 'w') as cache_file: + yaml.dump(self.to_dict(), cache_file) diff --git a/src/sinol_make/commands/gen/structs.py b/src/sinol_make/structs/gen_structs.py similarity index 100% rename from src/sinol_make/commands/gen/structs.py rename to src/sinol_make/structs/gen_structs.py diff --git a/src/sinol_make/commands/inwer/structs.py b/src/sinol_make/structs/inwer_structs.py similarity index 100% rename from src/sinol_make/commands/inwer/structs.py rename to src/sinol_make/structs/inwer_structs.py diff --git a/src/sinol_make/structs/run_structs.py b/src/sinol_make/structs/run_structs.py new file mode 100644 index 00000000..2488b1ee --- /dev/null +++ b/src/sinol_make/structs/run_structs.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass + + +@dataclass +class ExecutionData: + """ + Represents data for execution of a solution on a specified test. + """ + # Name of the solution + name: str + # Filename of the executable + executable: str + # Filename of the test + test: str + # Time limit for this test in milliseconds + time_limit: int + # Memory limit in KB + memory_limit: int + # Path to the timetool executable + timetool_path: str + + +@dataclass +class PrintData: + """ + Represents data for printing results of execution. + """ + + i: int diff --git a/src/sinol_make/structs/status_structs.py b/src/sinol_make/structs/status_structs.py index 5651e1de..d3f63c70 100644 --- a/src/sinol_make/structs/status_structs.py +++ b/src/sinol_make/structs/status_structs.py @@ -1,4 +1,6 @@ +from dataclasses import dataclass from enum import Enum +from typing import List class Status(str, Enum): @@ -15,3 +17,90 @@ def __str__(self): def __repr__(self): return self.name + + @staticmethod + def from_str(status): + if status == "CE": + return Status.CE + elif status == "TL": + return Status.TL + elif status == "ML": + return Status.ML + elif status == "RE": + return Status.RE + elif status == "WA": + return Status.WA + elif status == "OK": + return Status.OK + elif status == " ": + return Status.PENDING + else: + raise ValueError(f"Unknown status: '{status}'") + + +@dataclass +class ResultChange: + solution: str + group: int + old_result: Status + result: Status + + +@dataclass +class PointsChange: + solution: str + group: int + old_points: int + new_points: int + + +@dataclass +class ValidationResult: + added_solutions: set + removed_solutions: set + added_groups: set + removed_groups: set + changes: List[ResultChange] + expected_scores: dict + new_expected_scores: dict + + +@dataclass +class ExecutionResult: + # Result status of execution. Possible values + # can be found in `Status` enum definition. + Status: Status + # Time in milliseconds + Time: float + # Memory in KB + Memory: int + # Points for this test + Points: int + # Error message + Error: str + + def __init__(self, status=None, Time=None, Memory=None, Points=0, Error=None): + self.Status = status + self.Time = Time + self.Memory = Memory + self.Points = Points + self.Error = Error + + @staticmethod + def from_dict(dict): + return ExecutionResult( + status=Status.from_str(dict.get("Status", "")), + Time=dict.get("Time", None), + Memory=dict.get("Memory", None), + Points=dict.get("Points", 0), + Error=dict.get("Error", None) + ) + + def to_dict(self): + return { + "Status": str(self.Status), + "Time": self.Time, + "Memory": self.Memory, + "Points": self.Points, + "Error": self.Error + } diff --git a/tests/commands/gen/test_unit.py b/tests/commands/gen/test_unit.py index fe823cfd..8d069d32 100644 --- a/tests/commands/gen/test_unit.py +++ b/tests/commands/gen/test_unit.py @@ -4,7 +4,8 @@ import shutil import glob -from sinol_make.commands.gen import gen_util, OutputGenerationArguments +from sinol_make.commands.gen import gen_util +from sinol_make.structs.gen_structs import OutputGenerationArguments from sinol_make.helpers import package_util, compiler from tests import util from tests.fixtures import * diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index 9e28d86e..cc1df96b 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -4,6 +4,8 @@ import pytest import copy +from sinol_make.helpers import cache +from sinol_make.structs.cache_structs import CacheFile from ...fixtures import * from .util import * from sinol_make import configure_parsers, util, oiejq @@ -580,3 +582,113 @@ def test_flag_tests_not_existing_tests(create_package, time_tool, capsys): assert e.value.code == 1 out = capsys.readouterr().out assert "There are no tests to run." in out + + +@pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), + get_checker_package_path(), get_library_package_path(), + get_library_string_args_package_path(), get_limits_package_path(), + get_override_limits_package_path()], indirect=True) +def test_results_caching(create_package, time_tool): + """ + Test if test results are cached. + """ + package_path = create_package + create_ins_outs(package_path) + parser = configure_parsers() + args = parser.parse_args(["run", "--time-tool", time_tool]) + + def run(): + command = Command() + command.run(args) + return command + + start_time = time.time() + run() + length = time.time() - start_time + + start_time = time.time() + command = run() + end_time = time.time() - start_time + assert end_time - start_time < length / 2 + + task_id = package_util.get_task_id() + solutions = package_util.get_solutions(task_id, None) + for solution in solutions: + cache_file: CacheFile = cache.get_cache_file(solution) + for test in command.tests: + assert util.get_file_md5(test) in cache_file.tests + test_cache = cache_file.tests[util.get_file_md5(test)] + lang = package_util.get_file_lang(solution) + assert test_cache.time_limit == package_util.get_time_limit(test, command.config, lang, command.ID) + assert test_cache.memory_limit == package_util.get_memory_limit(test, command.config, lang, command.ID) + assert cache_file is not None + assert cache_file.tests != {} + + +@pytest.mark.parametrize("create_package", [get_checker_package_path()], indirect=True) +def test_results_caching_checker_changed(create_package, time_tool): + """ + Test if after changing checker source code, all cached test results are removed. + """ + package_path = create_package + create_ins_outs(package_path) + parser = configure_parsers() + args = parser.parse_args(["run", "--time-tool", time_tool]) + + # First run to cache test results. + command = Command() + command.run(args) + + # Change checker source code. + checker_source = "" + with open(os.path.join(os.getcwd(), "prog", "chkchk.cpp"), "r") as f: + checker_source = f.read() + with open(os.path.join(os.getcwd(), "prog", "chkchk.cpp"), "w") as f: + f.write("// Changed checker source code.\n" + checker_source) + + # Compile checker check if test results are removed. + command.compile_checker() + task_id = package_util.get_task_id() + solutions = package_util.get_solutions(task_id, None) + for solution in solutions: + cache_file: CacheFile = cache.get_cache_file(solution) + assert cache_file.tests == {} + + +@pytest.mark.parametrize("create_package", [get_library_package_path()], indirect=True) +def test_extra_compilation_files_change(create_package, time_tool): + """ + Test if after changing extra compilation files, all cached test results are removed. + """ + package_path = create_package + create_ins_outs(package_path) + parser = configure_parsers() + args = parser.parse_args(["run", "--time-tool", time_tool]) + command = Command() + + def change_file(file, comment_character): + with open(file, "r") as f: + source = f.read() + with open(file, "w") as f: + f.write(f"{comment_character} Changed source code.\n" + source) + + def test(file_to_change, lang, comment_character): + # First run to cache test results. + command.run(args) + + # Change file + change_file(os.path.join(os.getcwd(), "prog", file_to_change), comment_character) + + cache.save_to_cache_extra_compilation_files(command.config.get("extra_compilation_files", []), command.ID) + task_id = package_util.get_task_id() + solutions = package_util.get_solutions(task_id, None) + for solution in solutions: + if package_util.get_file_lang(solution) == lang: + print(file_to_change, solution) + assert not os.path.exists(paths.get_cache_path("md5sums", solution)) + info = cache.get_cache_file(solution) + assert info == CacheFile() + + test("liblib.cpp", "cpp", "//") + test("liblib.h", "cpp", "//") + test("liblib.py", "py", "#") diff --git a/tests/commands/run/test_unit.py b/tests/commands/run/test_unit.py index 683aa553..bb9502aa 100644 --- a/tests/commands/run/test_unit.py +++ b/tests/commands/run/test_unit.py @@ -1,8 +1,7 @@ import argparse, re, yaml from sinol_make import util, oiejq -from sinol_make.commands.run.structs import ResultChange, ValidationResult -from sinol_make.structs.status_structs import Status +from sinol_make.structs.status_structs import Status, ResultChange, ValidationResult from sinol_make.helpers import package_util from .util import * from ...util import * @@ -63,6 +62,7 @@ def test_run_solutions(create_package, time_tool): hide_memory=False) create_ins_outs(package_path) command.tests = package_util.get_tests("abc", None) + command.test_md5sums = {os.path.basename(test): util.get_file_md5(test) for test in command.tests} command.groups = list(sorted(set([command.get_group(test) for test in command.tests]))) command.scores = command.config["scores"] command.possible_score = command.get_possible_score(command.groups) diff --git a/tests/conftest.py b/tests/conftest.py index f5d6e0a6..9f97f993 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,26 @@ from typing import List import sys import glob +import yaml import os import pytest import multiprocessing as mp from sinol_make.helpers import compile, paths +from sinol_make.interfaces.Errors import CompilationError def _compile(args): package, file_path = args os.chdir(package) output = paths.get_executables_path(os.path.splitext(os.path.basename(file_path))[0] + ".e") - with open(paths.get_compilation_log_path(os.path.basename(file_path) + ".compile_log"), "w") as compile_log: - compile.compile(file_path, output, compile_log=compile_log) + compile_log_path = paths.get_compilation_log_path(os.path.basename(file_path) + ".compile_log") + try: + with open(compile_log_path, "w") as compile_log: + compile.compile(file_path, output, compile_log=compile_log) + except CompilationError: + compile.print_compile_log(compile_log_path) + raise def pytest_addoption(parser): @@ -31,11 +38,11 @@ def pytest_addoption(parser): def pytest_configure(config): + packages = glob.glob(os.path.join(os.path.dirname(__file__), "packages", "*")) if not config.getoption("--no-precompile"): print("Collecting solutions...") files_to_compile = [] - packages = glob.glob(os.path.join(os.path.dirname(__file__), "packages", "*")) for package in packages: if os.path.exists(os.path.join(package, "no-precompile")): print(f'Skipping precompilation for {package} due to no-precompile file') @@ -56,6 +63,25 @@ def pytest_configure(config): else: print("Skipping precompilation") + # We remove tests cache as it may interfere with testing. + for package in packages: + for md5sum_file in glob.glob(os.path.join(package, ".cache", "md5sums", "*")): + try: + with open(md5sum_file, "r") as f: + data = yaml.load(f, Loader=yaml.FullLoader) + + try: + if "tests" in data and data["tests"] != {}: + print(f"Removing tests cache for `{os.path.basename(md5sum_file)}`") + data["tests"] = {} + with open(md5sum_file, "w") as f: + yaml.dump(data, f) + except TypeError: + # Cache file is probably old/broken, we can delete it. + os.unlink(md5sum_file) + except FileNotFoundError: + pass + def pytest_generate_tests(metafunc): if "time_tool" in metafunc.fixturenames: diff --git a/tests/contest_types/test_default.py b/tests/contest_types/test_default.py index 6ec3b2eb..a67a68d3 100644 --- a/tests/contest_types/test_default.py +++ b/tests/contest_types/test_default.py @@ -1,5 +1,5 @@ from sinol_make import contest_types -from sinol_make.commands.run import ExecutionResult +from sinol_make.structs.status_structs import ExecutionResult def test_get_test_score(): diff --git a/tests/contest_types/test_oi.py b/tests/contest_types/test_oi.py index fe22dd0d..dcd3851d 100644 --- a/tests/contest_types/test_oi.py +++ b/tests/contest_types/test_oi.py @@ -1,5 +1,5 @@ from sinol_make import contest_types -from sinol_make.commands.run import ExecutionResult +from sinol_make.structs.status_structs import ExecutionResult def test_get_test_score(): diff --git a/tests/helpers/test_cache.py b/tests/helpers/test_cache.py index 4d7681ee..9f573065 100644 --- a/tests/helpers/test_cache.py +++ b/tests/helpers/test_cache.py @@ -1,7 +1,10 @@ import os import tempfile -from sinol_make.helpers import compile +from sinol_make.helpers import compile, paths +from sinol_make.helpers import cache +from sinol_make.structs.cache_structs import CacheFile, CacheTest +from sinol_make.structs.status_structs import ExecutionResult, Status def test_compilation_caching(): @@ -10,22 +13,84 @@ def test_compilation_caching(): program = os.path.join(tmpdir, 'program.cpp') open(program, 'w').write('int main() { return 0; }') - assert compile.check_compiled(program) is None + assert cache.check_compiled(program) is None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - exe_path = compile.check_compiled(program) + exe_path = cache.check_compiled(program) assert exe_path is not None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - exe_path2 = compile.check_compiled(program) + exe_path2 = cache.check_compiled(program) assert exe_path2 == exe_path open(program, 'w').write('int main() { return 1; }') - assert compile.check_compiled(program) is None + assert cache.check_compiled(program) is None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - assert compile.check_compiled(program) is not None + assert cache.check_compiled(program) is not None open(program, 'w').write('int main() { return 0; }') - assert compile.check_compiled(program) is None + assert cache.check_compiled(program) is None assert compile.compile(program, os.path.join(tmpdir, 'program'), compile_log=None) - assert compile.check_compiled(program) is not None + assert cache.check_compiled(program) is not None + + +def test_cache(): + with tempfile.TemporaryDirectory() as tmpdir: + os.chdir(tmpdir) + assert cache.get_cache_file("abc.cpp") == CacheFile() + + cache_file = CacheFile( + md5sum="md5sum", + executable_path="abc.e", + tests={ + "md5sum1": CacheTest( + time_limit=1000, + memory_limit=1024, + time_tool="time", + result=ExecutionResult( + status=Status.OK, + Time=0.5, + Memory=512, + Points=10, + ) + ), + "md5sum2": CacheTest( + time_limit=2000, + memory_limit=2048, + time_tool="time", + result=ExecutionResult( + status=Status.OK, + Time=1, + Memory=1024, + Points=20, + ) + ), + } + ) + + with open("abc.cpp", "w") as f: + f.write("int main() { return 0; }") + cache_file.save("abc.cpp") + assert cache.get_cache_file("abc.cpp") == cache_file + cache.save_compiled("abc.cpp", "abc.e", is_checker=True) + assert cache.get_cache_file("abc.cpp").tests == {} + + # Test that cache is cleared when extra compilation files change + cache_file.save("abc.cpp") + os.mkdir("prog") + with open("prog/abclib.cpp", "w") as f: + f.write("int main() { return 0; }") + + cache.save_to_cache_extra_compilation_files(["abclib.cpp"], "abc") + assert cache.get_cache_file("/some/very/long/path/abc.cpp") == CacheFile() + assert cache.get_cache_file("abclib.cpp") != CacheFile() + + cache_file.save("abc.cpp") + cache_file.save("abc.py") + with open("prog/abclib.cpp", "w") as f: + f.write("/* Changed file */ int main() { return 0; }") + cache.save_to_cache_extra_compilation_files(["abclib.cpp"], "abc") + assert not os.path.exists(paths.get_cache_path("md5sums", "abc.cpp")) + assert os.path.exists(paths.get_cache_path("md5sums", "abc.py")) + assert cache.get_cache_file("abc.py") == cache_file + assert cache.get_cache_file("abc.cpp") == CacheFile() diff --git a/tests/helpers/test_compile.py b/tests/helpers/test_compile.py index efca006f..0348c87a 100644 --- a/tests/helpers/test_compile.py +++ b/tests/helpers/test_compile.py @@ -1,7 +1,7 @@ import os import tempfile -from sinol_make.helpers import compile +from sinol_make.helpers.cache import save_compiled, check_compiled def test_compilation_caching(): @@ -12,7 +12,7 @@ def test_compilation_caching(): with open(os.path.join(os.getcwd(), "test.e"), "w") as f: f.write("") - assert compile.check_compiled(os.path.join(os.getcwd(), "test.txt")) is None - compile.save_compiled(os.path.join(os.getcwd(), "test.txt"), - os.path.join(os.getcwd(), "test.e")) - assert compile.check_compiled(os.path.join(os.getcwd(), "test.txt")) == os.path.join(os.getcwd(), "test.e") + assert check_compiled(os.path.join(os.getcwd(), "test.txt")) is None + save_compiled(os.path.join(os.getcwd(), "test.txt"), + os.path.join(os.getcwd(), "test.e")) + assert check_compiled(os.path.join(os.getcwd(), "test.txt")) == os.path.join(os.getcwd(), "test.e") diff --git a/tests/util.py b/tests/util.py index e6468b27..83c86bd6 100644 --- a/tests/util.py +++ b/tests/util.py @@ -80,7 +80,6 @@ def get_stack_size_package_path(): return os.path.join(os.path.dirname(__file__), "packages", "stc") - def get_override_limits_package_path(): """ Get path to package with `override_limits` present in config (/test/packages/ovl) From 25fe31e73164b0ca3883259f1424f605ad240f40 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 24 Sep 2023 12:31:09 +0200 Subject: [PATCH 4/5] Add ICPC contest type (#128) * Change format of `sinol_expected_scores` * Fix tests * Add icpc contest type * Add test for version change * Add icpc package * Add icpc package * Refactor * Add unit tests for icpc contest type * Refactor * Add description of `sinol_contest_type: icpc` * Update examples/config.yml Co-authored-by: Tomasz Nowak <36604952+tonowak@users.noreply.github.com> * Lower version * Remove printing of expected scores * Fix tests --------- Co-authored-by: Tomasz Nowak <36604952+tonowak@users.noreply.github.com> --- example_package/config.yml | 5 + src/sinol_make/__init__.py | 1 + src/sinol_make/commands/run/__init__.py | 98 +++---------- src/sinol_make/contest_types/__init__.py | 3 + src/sinol_make/contest_types/default.py | 82 ++++++++++- src/sinol_make/contest_types/icpc.py | 31 ++++ src/sinol_make/helpers/package_util.py | 29 ++-- src/sinol_make/util.py | 38 +++++ tests/commands/run/test_integration.py | 78 ++--------- tests/commands/run/test_unit.py | 132 ++++++++---------- tests/commands/run/util.py | 1 + tests/contest_types/test_default.py | 19 +++ tests/contest_types/test_icpc.py | 20 +++ tests/helpers/test_package_util.py | 9 +- tests/packages/abc/config.yml | 70 ++++++---- tests/packages/chk/config.yml | 13 +- tests/packages/example_tests/config.yml | 6 +- tests/packages/hwr/config.yml | 8 +- tests/packages/icpc/config.yml | 34 +++++ tests/packages/icpc/in/.gitkeep | 0 tests/packages/icpc/out/.gitkeep | 0 tests/packages/icpc/prog/abc4.cpp | 17 +++ tests/packages/icpc/prog/acm.cpp | 9 ++ tests/packages/icpc/prog/acm1.cpp | 12 ++ tests/packages/icpc/prog/acm2.cpp | 20 +++ tests/packages/icpc/prog/acm3.cpp | 34 +++++ tests/packages/icpc/prog/acmingen.cpp | 18 +++ tests/packages/lib/config.yml | 12 +- tests/packages/lim/config.yml | 20 +-- tests/packages/lsa/config.yml | 15 +- tests/packages/ovl/config.yml | 7 +- tests/packages/stc/config.yml | 5 +- .../packages/undocumented_options/config.yml | 9 +- tests/packages/vso/config.yml | 26 ++-- tests/packages/wcf/config.yml | 5 +- tests/util.py | 7 + tests/version_changes/__init__.py | 0 .../test_expected_scores_format_change.py | 31 ++++ 38 files changed, 617 insertions(+), 307 deletions(-) create mode 100644 src/sinol_make/contest_types/icpc.py create mode 100644 tests/contest_types/test_icpc.py create mode 100644 tests/packages/icpc/config.yml create mode 100644 tests/packages/icpc/in/.gitkeep create mode 100644 tests/packages/icpc/out/.gitkeep create mode 100644 tests/packages/icpc/prog/abc4.cpp create mode 100644 tests/packages/icpc/prog/acm.cpp create mode 100644 tests/packages/icpc/prog/acm1.cpp create mode 100644 tests/packages/icpc/prog/acm2.cpp create mode 100644 tests/packages/icpc/prog/acm3.cpp create mode 100644 tests/packages/icpc/prog/acmingen.cpp create mode 100644 tests/version_changes/__init__.py create mode 100644 tests/version_changes/test_expected_scores_format_change.py diff --git a/example_package/config.yml b/example_package/config.yml index b6228d8c..700f66ee 100644 --- a/example_package/config.yml +++ b/example_package/config.yml @@ -73,9 +73,14 @@ sinol_task_id: abc # Possible values are: # - `default` - Points for a test can only be 100 or 0 (unless checker assigns points). # Points for a group are calculated based of the lowest number of points for a test in this group. +# If scores are not defined in `scores` key, then all groups have the same number of points, +# summing up to 100. # - `oi` - Points for a test are unchanged if the execution time is less or equal to the time limit. # Otherwise, number of points decreases linearly to one depending on the execution time. # Points for a group are calculated same as in `default` mode. +# - `icpc` - A test passes when the status is OK. +# A group passes when all tests in this group pass. +# A solution passes when all groups pass. sinol_contest_type: oi # sinol-make can check if the solutions run as expected when using `run` command. diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index d54cc09a..15a5b656 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -61,6 +61,7 @@ def main_exn(): except Exception as err: util.exit_with_error('`oiejq` could not be installed.\n' + err) + util.make_version_changes() command.run(args) exit(0) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 5f305a5a..3bc4d649 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -147,9 +147,11 @@ def print_table_end(): util.color_red(group_status)), "%3s/%3s" % (points, scores[group]), end=" | ") - program_scores[program] += points if group_status == Status.OK else 0 program_groups_scores[program][group] = {"status": group_status, "points": points} print() + for program in program_group: + program_scores[program] = contest.get_global_score(program_groups_scores[program], possible_score) + print(8 * " ", end=" | ") for program in program_group: print(10 * " ", end=" | ") @@ -258,9 +260,9 @@ def configure_subparser(self, subparser): help=f'tool to measure time and memory usage (default: {default_timetool})') parser.add_argument('--oiejq-path', dest='oiejq_path', type=str, help='path to oiejq executable (default: `~/.local/bin/oiejq`)') - add_compilation_arguments(parser) parser.add_argument('-a', '--apply-suggestions', dest='apply_suggestions', action='store_true', help='apply suggestions from expected scores report') + add_compilation_arguments(parser) def parse_time(self, time_str): if len(time_str) < 3: return -1 @@ -638,7 +640,7 @@ def run_solutions(self, compiled_commands, names, solutions): for test in self.tests: all_results[name][self.get_group(test)][test] = ExecutionResult(Status.CE) print() - executions.sort(key = lambda x: (package_util.get_executable_key(x[1]), x[2])) + executions.sort(key = lambda x: (package_util.get_executable_key(x[1], self.ID), x[2])) program_groups_scores = collections.defaultdict(dict) print_data = PrintData(0) @@ -695,19 +697,6 @@ def run_solutions(self, compiled_commands, names, solutions): return program_groups_scores, all_results - - def calculate_points(self, results): - points = 0 - for group, result in results.items(): - if group != 0 and group not in self.scores: - util.exit_with_error(f'Group {group} doesn\'t have points specified in config file.') - if isinstance(result, str): - if result == Status.OK: - points += self.scores[group] - elif isinstance(result, dict): - points += result["points"] - return points - def compile_and_run(self, solutions): compilation_results = self.compile_solutions(solutions) for i in range(len(solutions)): @@ -734,10 +723,6 @@ def _convert(obj): return obj return _convert(dictionary) - def print_expected_scores(self, expected_scores): - yaml_dict = { "sinol_expected_scores": self.convert_status_to_string(expected_scores) } - print(yaml.dump(yaml_dict, default_flow_style=None)) - def get_whole_groups(self): """ Returns a list of groups for which all tests were run. @@ -770,33 +755,17 @@ def validate_expected_scores(self, results): if group not in self.scores: util.exit_with_error(f'Group {group} doesn\'t have points specified in config file.') - def convert_to_expected(results): - new_results = {} - for solution in results.keys(): - new_results[solution] = {} - for group, result in results[solution].items(): - if result["status"] == Status.OK: - if result["points"] == self.scores[group]: - new_results[solution][group] = Status.OK - else: - new_results[solution][group] = result - else: - new_results[solution][group] = result["status"] - return new_results - - results = convert_to_expected(results) - if self.checker is None: for solution in results.keys(): new_expected_scores[solution] = { "expected": results[solution], - "points": self.calculate_points(results[solution]) + "points": self.contest.get_global_score(results[solution], self.possible_score) } else: for solution in results.keys(): new_expected_scores[solution] = { "expected": results[solution], - "points": self.calculate_points(results[solution]) + "points": self.contest.get_global_score(results[solution], self.possible_score) } config_expected_scores = self.config.get("sinol_expected_scores", {}) @@ -812,8 +781,7 @@ def convert_to_expected(results): used_groups = set() if self.args.tests == None and config_expected_scores: # If no groups were specified, use all groups from config for solution in config_expected_scores.keys(): - for group in config_expected_scores[solution]["expected"]: - used_groups.add(group) + used_groups.update(config_expected_scores[solution]["expected"].keys()) else: used_groups = self.get_whole_groups() @@ -848,17 +816,11 @@ def convert_to_expected(results): if group in config_expected_scores[solution]["expected"]: expected_scores[solution]["expected"][group] = config_expected_scores[solution]["expected"][group] - expected_scores[solution]["points"] = self.calculate_points(expected_scores[solution]["expected"]) + expected_scores[solution]["points"] = self.contest.get_global_score(expected_scores[solution]["expected"], + self.possible_score) if len(expected_scores[solution]["expected"]) == 0: del expected_scores[solution] - if self.args.tests is not None: - print("Showing expected scores only for groups with all tests run.") - print(util.bold("Expected scores from config:")) - self.print_expected_scores(expected_scores) - print(util.bold("\nExpected scores based on results:")) - self.print_expected_scores(new_expected_scores) - expected_scores_diff = dictdiffer.diff(expected_scores, new_expected_scores) added_solutions = set() removed_solutions = set() @@ -961,11 +923,17 @@ def print_points_change(solution, group, new_points, old_points): def delete_group(solution, group): if group in config_expected_scores[solution]["expected"]: del config_expected_scores[solution]["expected"][group] - config_expected_scores[solution]["points"] = self.calculate_points(config_expected_scores[solution]["expected"]) + config_expected_scores[solution]["points"] = self.contest.get_global_score( + config_expected_scores[solution]["expected"], + self.possible_score + ) def set_group_result(solution, group, result): config_expected_scores[solution]["expected"][group] = result - config_expected_scores[solution]["points"] = self.calculate_points(config_expected_scores[solution]["expected"]) + config_expected_scores[solution]["points"] = self.contest.get_global_score( + config_expected_scores[solution]["expected"], + self.possible_score + ) if self.args.apply_suggestions: @@ -1055,33 +1023,7 @@ def set_scores(self): self.scores = collections.defaultdict(int) if 'scores' not in self.config.keys(): - print(util.warning('Scores are not defined in config.yml. Points will be assigned equally to all groups.')) - num_groups = len(self.groups) - self.scores = {} - if self.groups[0] == 0: - num_groups -= 1 - self.scores[0] = 0 - - # This only happens when running only on group 0. - if num_groups == 0: - self.possible_score = 0 - return - - points_per_group = 100 // num_groups - for group in self.groups: - if group == 0: - continue - self.scores[group] = points_per_group - - if points_per_group * num_groups != 100: - self.scores[self.groups[-1]] += 100 - points_per_group * num_groups - - print("Points will be assigned as follows:") - total_score = 0 - for group in self.scores: - print("%2d: %3d" % (group, self.scores[group])) - total_score += self.scores[group] - print() + self.scores = self.contest.assign_scores(self.groups) else: total_score = 0 for group in self.config["scores"]: @@ -1092,7 +1034,7 @@ def set_scores(self): print(util.warning("WARN: Scores sum up to %d instead of 100." % total_score)) print() - self.possible_score = self.get_possible_score(self.groups) + self.possible_score = self.contest.get_possible_score(self.groups, self.scores) def get_valid_input_files(self): """ diff --git a/src/sinol_make/contest_types/__init__.py b/src/sinol_make/contest_types/__init__.py index ea83390b..309198df 100644 --- a/src/sinol_make/contest_types/__init__.py +++ b/src/sinol_make/contest_types/__init__.py @@ -2,6 +2,7 @@ import yaml from sinol_make.contest_types.default import DefaultContest +from sinol_make.contest_types.icpc import ICPCContest from sinol_make.contest_types.oi import OIContest from sinol_make.interfaces.Errors import UnknownContestType @@ -15,5 +16,7 @@ def get_contest_type(): return DefaultContest() elif contest_type == "oi": return OIContest() + elif contest_type == "icpc": + return ICPCContest() else: raise UnknownContestType(f'Unknown contest type "{contest_type}"') diff --git a/src/sinol_make/contest_types/default.py b/src/sinol_make/contest_types/default.py index 727570a0..b55df0ba 100644 --- a/src/sinol_make/contest_types/default.py +++ b/src/sinol_make/contest_types/default.py @@ -1,6 +1,7 @@ from math import ceil -from typing import List +from typing import List, Dict +from sinol_make import util from sinol_make.structs.status_structs import ExecutionResult @@ -9,11 +10,86 @@ class DefaultContest: Default contest type. Points for tests are equal to points from execution result. Group score is equal to minimum score from tests. + Global score is sum of group scores. + Scores for groups are assigned equally. + Max possible score is sum of group scores. """ - def get_test_score(self, result: ExecutionResult, time_limit, memory_limit): + def assign_scores(self, groups: List[int]) -> Dict[int, int]: + """ + Returns dictionary with scores for each group. + Called if `scores` is not specified in config. + :param groups: List of groups + :return: Dictionary: {"": } + """ + print(util.warning('Scores are not defined in config.yml. Points will be assigned equally to all groups.')) + num_groups = len(groups) + scores = {} + if groups[0] == 0: + num_groups -= 1 + scores[0] = 0 + + # This only happens when running only on group 0. + if num_groups == 0: + return scores + + points_per_group = 100 // num_groups + for group in groups: + if group == 0: + continue + scores[group] = points_per_group + + if points_per_group * num_groups != 100: + scores[groups[-1]] += 100 - points_per_group * num_groups + + print("Points will be assigned as follows:") + total_score = 0 + for group in scores: + print("%2d: %3d" % (group, scores[group])) + total_score += scores[group] + print() + return scores + + def get_possible_score(self, groups: List[int], scores: Dict[int, int]) -> int: + """ + Get the maximum possible score. + :param groups: List of groups. + :param scores: Dictionary: {"": } + :return: Maximum possible score. + """ + if groups[0] == 0 and len(groups) == 1: + return 0 + + possible_score = 0 + for group in groups: + possible_score += scores[group] + return possible_score + + def get_test_score(self, result: ExecutionResult, time_limit, memory_limit) -> int: + """ + Returns points for test. + :param result: result of execution + :param time_limit: time limit for test + :param memory_limit: memory limit for test + :return: points for test + """ return result.Points - def get_group_score(self, test_scores: List[int], group_max_score): + def get_group_score(self, test_scores: List[int], group_max_score) -> int: + """ + Calculates group score based on tests scores. + :param test_scores: List of scores for tests + :param group_max_score: Maximum score for group + :return: + """ min_score = min(test_scores) return int(ceil(group_max_score * (min_score / 100.0))) + + def get_global_score(self, groups_scores: Dict[int, Dict], global_max_score) -> int: + """ + Calculates global score based on groups scores. + :param groups_scores: Dictionary: {": {"status": Status, "points": } + :param global_max_score: Maximum score for contest + :return: Global score + """ + return sum(group["points"] for group in groups_scores.values()) diff --git a/src/sinol_make/contest_types/icpc.py b/src/sinol_make/contest_types/icpc.py new file mode 100644 index 00000000..6031a75c --- /dev/null +++ b/src/sinol_make/contest_types/icpc.py @@ -0,0 +1,31 @@ +from typing import List, Dict + +from sinol_make.structs.status_structs import ExecutionResult +from sinol_make.contest_types import DefaultContest +from sinol_make.structs.status_structs import Status + + +class ICPCContest(DefaultContest): + """ + Contest type for ACM ICPC type contest. + The possible score for one solution is 1 or 0. + The score is 0 if any of the tests fail. + """ + + def assign_scores(self, groups: List[int]) -> Dict[int, int]: + return {group: 1 for group in groups} + + def get_possible_score(self, groups: List[int], scores: Dict[int, int]) -> int: + return 1 + + def get_test_score(self, result: ExecutionResult, time_limit, memory_limit): + if result.Status == Status.OK: + return 1 + else: + return 0 + + def get_group_score(self, test_scores, group_max_score): + return min(test_scores) + + def get_global_score(self, groups_scores: Dict[int, Dict], global_max_score): + return min(group["points"] for group in groups_scores.values()) diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index 3bc310a6..4afd68f9 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -41,6 +41,10 @@ def get_group(test_path, task_id): return int("".join(filter(str.isdigit, extract_test_id(test_path, task_id)))) +def get_groups(tests, task_id): + return sorted(list(set([get_group(test, task_id) for test in tests]))) + + def get_test_key(test, task_id): return get_group(test, task_id), test @@ -53,19 +57,26 @@ def get_solutions_re(task_id: str) -> re.Pattern: return re.compile(r"^%s[bs]?[0-9]*\.(cpp|cc|java|py|pas)$" % task_id) -def get_executable_key(executable): +def get_executable_key(executable, task_id): name = get_file_name(executable) + task_id_len = len(task_id) value = [0, 0] - if name[3] == 's': + if name[task_id_len] == 's': value[0] = 1 - suffix = name.split(".")[0][4:] - elif name[3] == 'b': + suffix = name.split(".")[0][(task_id_len + 1):] + elif name[task_id_len] == 'b': value[0] = 2 - suffix = name.split(".")[0][4:] + suffix = name.split(".")[0][(task_id_len + 1):] else: - suffix = name.split(".")[0][3:] + suffix = name.split(".")[0][task_id_len:] if suffix != "": - value[1] = int(suffix) + i = 0 + digits = "" + while i < len(suffix) and suffix[i].isdigit(): + digits += suffix[i] + i += 1 + if digits != "": + value[1] = int(digits) return tuple(value) @@ -123,7 +134,7 @@ def get_solutions(task_id: str, args_solutions: Union[List[str], None] = None) - if args_solutions is None: solutions = [solution for solution in os.listdir("prog/") if solutions_re.match(solution)] - return sorted(solutions, key=get_executable_key) + return sorted(solutions, key=lambda solution: get_executable_key(solution, task_id)) else: solutions = [] for solution in get_files_matching(args_solutions, "prog"): @@ -132,7 +143,7 @@ def get_solutions(task_id: str, args_solutions: Union[List[str], None] = None) - if solutions_re.match(os.path.basename(solution)) is not None: solutions.append(os.path.basename(solution)) - return sorted(solutions, key=get_executable_key) + return sorted(solutions, key=lambda solution: get_executable_key(solution, task_id)) def get_file_name(file_path): diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index 68c2f4f9..ab0e7167 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -9,6 +9,7 @@ from typing import Union import sinol_make +from sinol_make.contest_types import get_contest_type def get_commands(): @@ -286,6 +287,43 @@ def get_file_md5(path): return hashlib.md5(f.read()).hexdigest() +def make_version_changes(): + if compare_versions(sinol_make.__version__, "1.5.8") == 1: + # In version 1.5.9 we changed the format of sinol_expected_scores. + # Now all groups have specified points and status. + + if check_if_package(): + with open("config.yml", "r") as config_file: + config = yaml.load(config_file, Loader=yaml.FullLoader) + + try: + new_expected_scores = {} + expected_scores = config["sinol_expected_scores"] + contest = get_contest_type() + groups = [] + for solution, results in expected_scores.items(): + for group in results["expected"].keys(): + if group not in groups: + groups.append(int(group)) + + scores = contest.assign_scores(groups) + for solution, results in expected_scores.items(): + new_expected_scores[solution] = {"expected": {}, "points": results["points"]} + for group, result in results["expected"].items(): + new_expected_scores[solution]["expected"][group] = {"status": result} + if result == "OK": + new_expected_scores[solution]["expected"][group]["points"] = scores[group] + else: + new_expected_scores[solution]["expected"][group]["points"] = 0 + config["sinol_expected_scores"] = new_expected_scores + save_config(config) + except: + # If there is an error, we just delete the field. + if "sinol_expected_scores" in config: + del config["sinol_expected_scores"] + save_config(config) + + def color_red(text): return "\033[91m{}\033[00m".format(text) def color_green(text): return "\033[92m{}\033[00m".format(text) def color_yellow(text): return "\033[93m{}\033[00m".format(text) diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index cc1df96b..84dd9b19 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -14,7 +14,7 @@ @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), get_checker_package_path(), get_library_package_path(), get_library_string_args_package_path(), get_limits_package_path(), - get_override_limits_package_path()], + get_override_limits_package_path(), get_icpc_package_path()], indirect=True) def test_simple(create_package, time_tool): """ @@ -33,7 +33,7 @@ def test_simple(create_package, time_tool): @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), get_checker_package_path(), get_library_package_path(), get_library_string_args_package_path(), get_limits_package_path(), - get_override_limits_package_path()], + get_override_limits_package_path(), get_icpc_package_path()], indirect=True) def test_no_expected_scores(capsys, create_package, time_tool): """ @@ -69,7 +69,7 @@ def test_no_expected_scores(capsys, create_package, time_tool): @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), get_checker_package_path(), get_library_package_path(), get_library_string_args_package_path(), get_limits_package_path(), - get_override_limits_package_path()], + get_override_limits_package_path(), get_icpc_package_path()], indirect=True) def test_apply_suggestions(create_package, time_tool): """ @@ -110,7 +110,7 @@ def test_incorrect_expected_scores(capsys, create_package, time_tool): config_path = os.path.join(package_path, "config.yml") with open(config_path, "r") as config_file: config = yaml.load(config_file, Loader=yaml.SafeLoader) - config["sinol_expected_scores"]["abc.cpp"]["expected"][1] = "WA" + config["sinol_expected_scores"]["abc.cpp"]["expected"][1] = {"status": "WA", "points": 0} config["sinol_expected_scores"]["abc.cpp"]["points"] = 75 with open(config_path, "w") as config_file: config_file.write(yaml.dump(config)) @@ -130,7 +130,8 @@ def test_incorrect_expected_scores(capsys, create_package, time_tool): @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_checker_package_path(), - get_library_package_path(), get_library_string_args_package_path()], + get_library_package_path(), get_library_string_args_package_path(), + get_icpc_package_path()], indirect=True) def test_flag_tests(create_package, time_tool): """ @@ -152,68 +153,8 @@ def test_flag_tests(create_package, time_tool): assert command.tests == [os.path.join("in", os.path.basename(test))] -@pytest.mark.parametrize("create_package", [get_checker_package_path()], indirect=True) -def test_groups_in_flag_test(capsys, create_package, time_tool): - """ - Test flag --tests with whole and partial groups. - """ - package_path = create_package - create_ins_outs(package_path) - - parser = configure_parsers() - - # Test with only one test from group 1. - args = parser.parse_args(["run", "--tests", "in/chk1a.in", "--time-tool", time_tool]) - command = Command() - command.run(args) - out = capsys.readouterr().out - assert "Showing expected scores only for groups with all tests run." in out - assert "sinol_expected_scores: {}" in out - assert "Expected scores are correct!" in out - - # Test with all tests from group 1. - args = parser.parse_args(["run", "--tests", "in/chk1a.in", "in/chk1b.in", "in/chk1c.in", "--time-tool", time_tool]) - command = Command() - command.run(args) - out = capsys.readouterr().out - assert 'sinol_expected_scores:\n' \ - ' chk.cpp:\n' \ - ' expected: {1: OK}\n' \ - ' points: 50\n' \ - ' chk1.cpp:\n' \ - ' expected: {1: WA}\n' \ - ' points: 0\n' \ - ' chk2.cpp:\n' \ - ' expected:\n' \ - ' 1: {points: 25, status: OK}\n' \ - ' points: 25\n' \ - ' chk3.cpp:\n' \ - ' expected: {1: OK}\n' \ - ' points: 50' in out - - # Test with incorrect expected scores for first group. - with open(os.path.join(package_path, "config.yml"), "r") as config_file: - correct_config = yaml.load(config_file, Loader=yaml.SafeLoader) - config = copy.deepcopy(correct_config) - config["sinol_expected_scores"]["chk.cpp"]["expected"][1] = "WA" - config["sinol_expected_scores"]["chk.cpp"]["points"] = 50 - with open(os.path.join(package_path, "config.yml"), "w") as config_file: - config_file.write(yaml.dump(config)) - - args = parser.parse_args(["run", "--tests", "in/chk1a.in", "in/chk1b.in", "in/chk1c.in", "--time-tool", time_tool, - "--apply-suggestions"]) - command = Command() - command.run(args) - out = capsys.readouterr().out - sys.stdout.write(out) - assert "Solution chk.cpp passed group 1 with status OK while it should pass with status WA." in out - with open(os.path.join(package_path, "config.yml"), "r") as config_file: - config = yaml.load(config_file, Loader=yaml.SafeLoader) - assert config == correct_config - - @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path(), - get_checker_package_path()], indirect=True) + get_checker_package_path(), get_icpc_package_path()], indirect=True) def test_flag_solutions(capsys, create_package, time_tool): """ Test flag --solutions. @@ -412,7 +353,6 @@ def test_override_limits(create_package, time_tool): create_ins_outs(package_path) config_file_path = os.path.join(package_path, "config.yml") - # With `override_limits` key deleted. with open(config_file_path, "r") as config_file: original_config = yaml.load(config_file, Loader=yaml.SafeLoader) @@ -430,7 +370,7 @@ def test_override_limits(create_package, time_tool): assert config["sinol_expected_scores"] == { "ovl.cpp": { - "expected": {1: "TL", 2: "TL"}, + "expected": {1: {"status": "TL", "points": 0}, 2: {"status": "TL", "points": 0}}, "points": 0 } } @@ -451,7 +391,7 @@ def test_override_limits(create_package, time_tool): assert config["sinol_expected_scores"] == { "ovl.cpp": { - "expected": {1: "ML", 2: "ML"}, + "expected": {1: {"status": "ML", "points": 0}, 2: {"status": "ML", "points": 0}}, "points": 0 } } diff --git a/tests/commands/run/test_unit.py b/tests/commands/run/test_unit.py index bb9502aa..5e2d2a87 100644 --- a/tests/commands/run/test_unit.py +++ b/tests/commands/run/test_unit.py @@ -43,18 +43,6 @@ def test_execution(create_package, time_tool): assert result.Status == Status.OK -def test_calculate_points(): - os.chdir(get_simple_package_path()) - command = get_command() - command.scores = command.config["scores"] - - assert command.calculate_points({1: Status.OK, 2: Status.OK, 3: Status.OK, 4: Status.OK}) == 100 - assert command.calculate_points({1: Status.OK, 2: Status.OK, 3: Status.OK, 4: Status.WA}) == 75 - assert command.calculate_points({1: Status.OK, 2: Status.OK, 3: Status.TL}) == 50 - assert command.calculate_points({1: Status.OK}) == 25 - assert command.calculate_points({1: Status.WA}) == 0 - - def test_run_solutions(create_package, time_tool): package_path = create_package command = get_command(package_path) @@ -84,42 +72,16 @@ def flatten_results(results): } -def test_print_expected_scores(capsys): - os.chdir(get_simple_package_path()) - command = get_command() - expected_scores = """sinol_expected_scores: - abc.cpp: - expected: {1: "OK", 2: "OK", 3: "OK", 4: "OK"} - points: 100 - abc1.cpp: - expected: {1: "OK", 2: "OK", 3: "OK", 4: "WA"} - points: 75 - abc2.cpp: - expected: {1: "OK", 2: "WA", 3: "WA", 4: "TL"} - points: 25 - abc3.cpp: - expected: {1: "OK", 2: "WA", 3: "WA", 4: "ML"} - points: 25 - abc4.cpp: - expected: {1: "OK", 2: "OK", 3: "WA", 4: "RE"} - points: 50 - -""" - - expected_scores_dict = yaml.load(expected_scores, Loader=yaml.FullLoader) - command.print_expected_scores(expected_scores_dict["sinol_expected_scores"]) - out = capsys.readouterr().out - assert expected_scores.replace('"', '') in out - - def test_validate_expected_scores_success(): os.chdir(get_simple_package_path()) command = get_command() command.scores = command.config["scores"] - command.tests = package_util.get_tests("abc", None) + command.tests = ["in/abc1a.in", "in/abc2a.in", "in/abc3a.in", "in/abc4a.in"] + command.groups = command.get_groups(command.tests) + command.possible_score = command.contest.get_possible_score(command.groups, command.scores) # Test with correct expected scores. - command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=None) + command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=None, print_expected_scores=True) results = { "abc.cpp": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, 3: {"status": "OK", "points": 25}, 4: {"status": "OK", "points": 25}}, } @@ -128,16 +90,16 @@ def test_validate_expected_scores_success(): assert results.removed_solutions == set() # Test with incorrect result. - command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=None) + command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=None, print_expected_scores=True) results = { "abc.cpp": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, 3: {"status": "OK", "points": 25}, 4: {"status": "WA", "points": 0}}, } results = command.validate_expected_scores(results) assert results.expected_scores != results.new_expected_scores - assert len(results.changes) == 1 + assert len(results.changes) == 2 # Test with removed solution. - command.args = argparse.Namespace(solutions=None, tests=None) + command.args = argparse.Namespace(solutions=None, tests=None, print_expected_scores=True) results = { "abc.cpp": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, 3: {"status": "OK", "points": 25}, 4: {"status": "OK", "points": 25}}, "abc1.cpp": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, 3: {"status": "OK", "points": 25}, 4: {"status": "WA", "points": 0}}, @@ -150,7 +112,8 @@ def test_validate_expected_scores_success(): # Test with added solution and added group. command.config["scores"][5] = 0 - command.args = argparse.Namespace(solutions=["prog/abc.cpp", "prog/abc5.cpp"], tests=None) + command.args = argparse.Namespace(solutions=["prog/abc.cpp", "prog/abc5.cpp"], tests=None, + print_expected_scores=True) results = { "abc.cpp": {1: {"status": "OK", "points": 20}, 2: {"status": "OK", "points": 20}, 3: {"status": "OK", "points": 20}, 4: {"status": "OK", "points": 20}, 5: {"status": "WA", "points": 0}}, "abc5.cpp": {1: {"status": "OK", "points": 20}, 2: {"status": "OK", "points": 20}, 3: {"status": "OK", "points": 20}, 4: {"status": "OK", "points": 20}, 5: {"status": "WA", "points": 0}}, @@ -161,7 +124,7 @@ def test_validate_expected_scores_success(): assert len(results.added_groups) == 1 # Test with removed group. - command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=None) + command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=None, print_expected_scores=True) results = { "abc.cpp": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, 3: {"status": "OK", "points": 25}}, } @@ -170,7 +133,8 @@ def test_validate_expected_scores_success(): assert len(results.removed_groups) == 1 # Test with correct expected scores and --tests flag. - command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=["in/abc1a.in", "in/abc2a.in"]) + command.args = argparse.Namespace(solutions=["prog/abc.cpp"], tests=["in/abc1a.in", "in/abc2a.in"], + print_expected_scores=True) results = { "abc.cpp": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}}, } @@ -201,6 +165,7 @@ def test_print_expected_scores_diff(capsys, create_package): package_path = create_package command = get_command(package_path) command.args = argparse.Namespace(apply_suggestions=False) + command.possible_score = 100 # Test with correct expected scores. results = ValidationResult( @@ -210,10 +175,12 @@ def test_print_expected_scores_diff(capsys, create_package): removed_groups=set(), changes=[], expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, }, new_expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, } ) command.print_expected_scores_diff(results) @@ -228,10 +195,12 @@ def test_print_expected_scores_diff(capsys, create_package): removed_groups=set(), changes=[ResultChange("abc.cpp", 1, "OK", "WA")], expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, }, new_expected_scores={ - "abc.cpp": {1: "WA", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "WA", "points": 0}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, } ) with pytest.raises(SystemExit) as e: @@ -249,11 +218,14 @@ def test_print_expected_scores_diff(capsys, create_package): removed_groups=set(), changes=[], expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, }, new_expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, - "abc5.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, + "abc5.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, } ) with pytest.raises(SystemExit) as e: @@ -271,11 +243,14 @@ def test_print_expected_scores_diff(capsys, create_package): removed_groups=set(), changes=[], expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, - "abc5.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, + "abc5.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, }, new_expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, } ) with pytest.raises(SystemExit) as e: @@ -293,10 +268,13 @@ def test_print_expected_scores_diff(capsys, create_package): removed_groups=set(), changes=[], expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, }, new_expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK", 5: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}, + 5: {"status": "OK", "points": 100}}, } ) with pytest.raises(SystemExit) as e: @@ -314,10 +292,13 @@ def test_print_expected_scores_diff(capsys, create_package): removed_groups={5}, changes=[], expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK", 5: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}, + 5: {"status": "OK", "points": 100}}, }, new_expected_scores={ - "abc.cpp": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "abc.cpp": {1: {"status": "OK", "points": 100}, 2: {"status": "OK", "points": 100}, + 3: {"status": "OK", "points": 100}, 4: {"status": "OK", "points": 100}}, } ) with pytest.raises(SystemExit) as e: @@ -339,21 +320,25 @@ def test_print_expected_scores_diff(capsys, create_package): changes=[ResultChange("abc.cpp", 1, "OK", "WA")], expected_scores={ "abc.cpp": { - "expected": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}, 4: {"status": "OK", "points": 25}}, "points": 100 }, "abc4.cpp": { - "expected": {1: "OK", 2: "OK", 3: "OK", 4: "OK"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}, 4: {"status": "OK", "points": 25}}, "points": 100 } }, new_expected_scores={ "abc.cpp": { - "expected": {1: "WA", 2: "OK", 3: "OK", 5: "OK"}, + "expected": {1: {"status": "WA", "points": 0}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}, 5: {"status": "OK", "points": 0}}, "points": 50 }, "abc5.cpp": { - "expected": {1: "OK", 2: "OK", 3: "OK", 5: "OK"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}, 5: {"status": "OK", "points": 0}}, "points": 75 } } @@ -371,23 +356,28 @@ def test_print_expected_scores_diff(capsys, create_package): config = yaml.load(config_file, Loader=yaml.FullLoader) assert config["sinol_expected_scores"] == { "abc.cpp": { - "expected": {1: "WA", 2: "OK", 3: "OK", 5: "OK"}, + "expected": {1: {"status": "WA", "points": 0}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}, 5: {"status": "OK", "points": 0}}, "points": 50 }, "abc1.cpp": { - "expected": {1: "OK", 2: "OK", 3: "OK"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}}, "points": 75 }, "abc2.cpp": { - "expected": {1: "OK", 2: "WA", 3: "WA"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "WA", "points": 0}, + 3: {"status": "WA", "points": 0}}, "points": 25 }, "abc3.cpp": { - "expected": {1: "OK", 2: "WA", 3: "WA"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "WA", "points": 0}, + 3: {"status": "WA", "points": 0}}, "points": 25 }, "abc5.cpp": { - "expected": {1: "OK", 2: "OK", 3: "OK", 5: "OK"}, + "expected": {1: {"status": "OK", "points": 25}, 2: {"status": "OK", "points": 25}, + 3: {"status": "OK", "points": 25}, 5: {"status": "OK", "points": 0}}, "points": 75 } } diff --git a/tests/commands/run/util.py b/tests/commands/run/util.py index d3e063f7..ea66f0f8 100644 --- a/tests/commands/run/util.py +++ b/tests/commands/run/util.py @@ -37,4 +37,5 @@ def get_command(path = None): def set_default_args(command): command.args = argparse.Namespace( weak_compilation_flags=False, + print_expected_scores=True, ) diff --git a/tests/contest_types/test_default.py b/tests/contest_types/test_default.py index a67a68d3..928107d8 100644 --- a/tests/contest_types/test_default.py +++ b/tests/contest_types/test_default.py @@ -16,3 +16,22 @@ def test_get_group_score(): assert contest.get_group_score([50, 100, 100], 100) == 50 assert contest.get_group_score([0, 10, 20], 100) == 0 assert contest.get_group_score([10, 3, 5], 50) == 2 + + +def test_assign_scores(): + contest = contest_types.DefaultContest() + assert contest.assign_scores([0, 1, 2, 3]) == {0: 0, 1: 33, 2: 33, 3: 34} + assert contest.assign_scores([1, 2, 3]) == {1: 33, 2: 33, 3: 34} + assert contest.assign_scores([0, 1, 2]) == {0: 0, 1: 50, 2: 50} + + +def test_get_possible_score(): + contest = contest_types.DefaultContest() + assert contest.get_possible_score([0, 1, 2, 3], {0: 0, 1: 33, 2: 33, 3: 34}) == 100 + assert contest.get_possible_score([1, 2, 3], {1: 33, 2: 33, 3: 34}) == 100 + assert contest.get_possible_score([0, 2], {0: 0, 1: 50, 2: 50}) == 50 + + +def test_get_global_score(): + contest = contest_types.DefaultContest() + assert contest.get_global_score({1: {"points": 10}, 2: {"points": 20}}, 100) == 30 diff --git a/tests/contest_types/test_icpc.py b/tests/contest_types/test_icpc.py new file mode 100644 index 00000000..0c7a3e04 --- /dev/null +++ b/tests/contest_types/test_icpc.py @@ -0,0 +1,20 @@ +from sinol_make import contest_types + + +def test_assign_scores(): + contest = contest_types.ICPCContest() + assert contest.assign_scores([0, 1, 2, 3]) == {0: 1, 1: 1, 2: 1, 3: 1} + assert contest.assign_scores([1, 2, 3]) == {1: 1, 2: 1, 3: 1} + assert contest.assign_scores([0, 1, 2]) == {0: 1, 1: 1, 2: 1} + + +def test_get_group_score(): + contest = contest_types.ICPCContest() + assert contest.get_group_score([1, 1, 1, 1], 1) == 1 + assert contest.get_group_score([1, 0, 1], 1) == 0 + + +def test_get_global_score(): + contest = contest_types.ICPCContest() + assert contest.get_global_score({1: {"points": 1}, 2: {"points": 1}}, 1) == 1 + assert contest.get_global_score({1: {"points": 1}, 2: {"points": 0}}, 1) == 0 diff --git a/tests/helpers/test_package_util.py b/tests/helpers/test_package_util.py index 44ca3ce4..d821eb17 100644 --- a/tests/helpers/test_package_util.py +++ b/tests/helpers/test_package_util.py @@ -231,7 +231,14 @@ def test_validate_files(create_package, capsys): def test_get_executable_key(): os.chdir(get_simple_package_path()) - assert package_util.get_executable_key("abc1.cpp.e") == (0, 1) + for task_id in ["abc", "long_task_id", "", "x"]: + assert package_util.get_executable_key(f"{task_id}1.cpp.e", task_id) == (0, 1) + assert package_util.get_executable_key(f"{task_id}2.cpp.e", task_id) == (0, 2) + assert package_util.get_executable_key(f"{task_id}s20.cpp.e", task_id) == (1, 20) + assert package_util.get_executable_key(f"{task_id}s21.cpp.e", task_id) == (1, 21) + assert package_util.get_executable_key(f"{task_id}b100.cpp.e", task_id) == (2, 100) + assert package_util.get_executable_key(f"{task_id}b101.cpp.e", task_id) == (2, 101) + assert package_util.get_executable_key(f"{task_id}x1000.cpp.e", task_id) == (0, 0) def test_get_solutions(): diff --git a/tests/packages/abc/config.yml b/tests/packages/abc/config.yml index 46c73673..e660100e 100644 --- a/tests/packages/abc/config.yml +++ b/tests/packages/abc/config.yml @@ -1,26 +1,44 @@ -title: Simple test package -memory_limit: 16000 -time_limit: 1000 - -scores: - 1: 25 - 2: 25 - 3: 25 - 4: 25 - -sinol_expected_scores: - abc.cpp: - points: 100 - expected: {1: "OK", 2: "OK", 3: "OK", 4: "OK"} - abc1.cpp: - points: 75 - expected: {1: "OK", 2: "OK", 3: "OK", 4: "WA"} - abc2.cpp: - points: 25 - expected: {1: "OK", 2: "WA", 3: "WA", 4: "TL"} - abc3.cpp: - points: 25 - expected: {1: "OK", 2: "WA", 3: "WA", 4: "ML"} - abc4.cpp: - points: 50 - expected: {1: "OK", 2: "OK", 3: "WA", 4: "RE"} +title: Simple test package +memory_limit: 16000 +time_limit: 1000 +scores: + 1: 25 + 2: 25 + 3: 25 + 4: 25 +sinol_expected_scores: + abc.cpp: + expected: + 1: {points: 25, status: OK} + 2: {points: 25, status: OK} + 3: {points: 25, status: OK} + 4: {points: 25, status: OK} + points: 100 + abc1.cpp: + expected: + 1: {points: 25, status: OK} + 2: {points: 25, status: OK} + 3: {points: 25, status: OK} + 4: {points: 0, status: WA} + points: 75 + abc2.cpp: + expected: + 1: {points: 25, status: OK} + 2: {points: 0, status: WA} + 3: {points: 0, status: WA} + 4: {points: 0, status: TL} + points: 25 + abc3.cpp: + expected: + 1: {points: 25, status: OK} + 2: {points: 0, status: WA} + 3: {points: 0, status: WA} + 4: {points: 0, status: ML} + points: 25 + abc4.cpp: + expected: + 1: {points: 25, status: OK} + 2: {points: 25, status: OK} + 3: {points: 0, status: WA} + 4: {points: 0, status: RE} + points: 50 diff --git a/tests/packages/chk/config.yml b/tests/packages/chk/config.yml index fe3d3468..706b92f6 100644 --- a/tests/packages/chk/config.yml +++ b/tests/packages/chk/config.yml @@ -4,12 +4,17 @@ time_limit: 1000 scores: 1: 50 2: 50 + sinol_expected_scores: chk.cpp: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 chk1.cpp: - expected: {1: WA, 2: WA} + expected: + 1: {points: 0, status: WA} + 2: {points: 0, status: WA} points: 0 chk2.cpp: expected: @@ -17,5 +22,7 @@ sinol_expected_scores: 2: {points: 25, status: OK} points: 50 chk3.cpp: - expected: {1: OK, 2: WA} + expected: + 1: {points: 50, status: OK} + 2: {points: 0, status: WA} points: 50 diff --git a/tests/packages/example_tests/config.yml b/tests/packages/example_tests/config.yml index 1ddcd9af..1b480e60 100644 --- a/tests/packages/example_tests/config.yml +++ b/tests/packages/example_tests/config.yml @@ -1,8 +1,10 @@ title: Package with example tests +sinol_task_id: exa memory_limit: 10240 time_limit: 1000 + sinol_expected_scores: exa.cpp: - expected: {0: OK} + expected: + 0: {points: 0, status: OK} points: 0 -sinol_task_id: exa diff --git a/tests/packages/hwr/config.yml b/tests/packages/hwr/config.yml index 9b4efe1e..9399952c 100644 --- a/tests/packages/hwr/config.yml +++ b/tests/packages/hwr/config.yml @@ -1,13 +1,13 @@ title: Package with handwritten tests for testing `export` command - memory_limit: 32000 time_limit: 1 - scores: 1: 50 2: 50 - sinol_expected_scores: hwr.cpp: - expected: {0: OK, 1: OK, 2: OK} + expected: + 0: {points: 0, status: OK} + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 diff --git a/tests/packages/icpc/config.yml b/tests/packages/icpc/config.yml new file mode 100644 index 00000000..be144ecb --- /dev/null +++ b/tests/packages/icpc/config.yml @@ -0,0 +1,34 @@ +title: Package for testing ICPC contest +sinol_task_id: acm +sinol_contest_type: icpc +memory_limit: 16000 +time_limit: 1000 +sinol_expected_scores: + acm.cpp: + expected: + 1: {points: 1, status: OK} + 2: {points: 1, status: OK} + 3: {points: 1, status: OK} + 4: {points: 1, status: OK} + points: 1 + acm1.cpp: + expected: + 1: {points: 1, status: OK} + 2: {points: 1, status: OK} + 3: {points: 1, status: OK} + 4: {points: 0, status: WA} + points: 0 + acm2.cpp: + expected: + 1: {points: 1, status: OK} + 2: {points: 0, status: WA} + 3: {points: 0, status: WA} + 4: {points: 0, status: TL} + points: 0 + acm3.cpp: + expected: + 1: {points: 1, status: OK} + 2: {points: 0, status: WA} + 3: {points: 0, status: WA} + 4: {points: 0, status: ML} + points: 0 diff --git a/tests/packages/icpc/in/.gitkeep b/tests/packages/icpc/in/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/icpc/out/.gitkeep b/tests/packages/icpc/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/icpc/prog/abc4.cpp b/tests/packages/icpc/prog/abc4.cpp new file mode 100644 index 00000000..f339f90d --- /dev/null +++ b/tests/packages/icpc/prog/abc4.cpp @@ -0,0 +1,17 @@ +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + if (a == 1 || a == 2) + cout << a + b; + else if (a == 3) + cout << a + b + 2; + else if (a == 4) { + int c = 0; + cout << a + b / c; + return 1; + } +} diff --git a/tests/packages/icpc/prog/acm.cpp b/tests/packages/icpc/prog/acm.cpp new file mode 100644 index 00000000..9d30bab0 --- /dev/null +++ b/tests/packages/icpc/prog/acm.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + cout << a + b; +} diff --git a/tests/packages/icpc/prog/acm1.cpp b/tests/packages/icpc/prog/acm1.cpp new file mode 100644 index 00000000..c1b5782e --- /dev/null +++ b/tests/packages/icpc/prog/acm1.cpp @@ -0,0 +1,12 @@ +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + if (a == 4) + cout << a + b + 1; + else + cout << a + b; +} diff --git a/tests/packages/icpc/prog/acm2.cpp b/tests/packages/icpc/prog/acm2.cpp new file mode 100644 index 00000000..6979f775 --- /dev/null +++ b/tests/packages/icpc/prog/acm2.cpp @@ -0,0 +1,20 @@ +#include + +using namespace std; + +int main() { + int a, b; + cin >> a >> b; + if (a == 1) + cout << a + b; + else if (a == 2 || a == 3) + cout << a + b - 1; + else if (a == 4) { + time_t start = time(0); + int i = 0; + while (time(0) - start < 5) { + i++; + } + cout << a + b; + } +} diff --git a/tests/packages/icpc/prog/acm3.cpp b/tests/packages/icpc/prog/acm3.cpp new file mode 100644 index 00000000..129e4932 --- /dev/null +++ b/tests/packages/icpc/prog/acm3.cpp @@ -0,0 +1,34 @@ +#include + +using namespace std; + +int rnd() { + return rand() % 100; +} + +int main() { + int a, b; + cin >> a >> b; + if (a == 1) + cout << a + b; + else if (a == 2 || a == 3) + cout << a + b + 2; + else if (a == 4) { + vector v; + for (int i = 0; i <= 10000; i++) { + int *tmp = new int[1000]; + for (int j = 0; j < 1000; j++) { + tmp[j] = rnd(); + } + v.push_back(tmp); + } + int s = 0; + for (auto i : v) { + for (int j = 0; j < 1000; j++) { + s = (s + i[j]) % 1000000007; + } + delete[] i; + } + cout << a + b; + } +} diff --git a/tests/packages/icpc/prog/acmingen.cpp b/tests/packages/icpc/prog/acmingen.cpp new file mode 100644 index 00000000..bdee7ab2 --- /dev/null +++ b/tests/packages/icpc/prog/acmingen.cpp @@ -0,0 +1,18 @@ +#include + +using namespace std; + +int main() { + ofstream f("acm1a.in"); + f << "1 3\n"; + f.close(); + f.open("acm2a.in"); + f << "2 5\n"; + f.close(); + f.open("acm3a.in"); + f << "3 7\n"; + f.close(); + f.open("acm4a.in"); + f << "4 9\n"; + f.close(); +} diff --git a/tests/packages/lib/config.yml b/tests/packages/lib/config.yml index ce668c12..a1131fba 100644 --- a/tests/packages/lib/config.yml +++ b/tests/packages/lib/config.yml @@ -9,11 +9,17 @@ extra_compilation_args: cpp: [liblib.cpp] sinol_expected_scores: lib.cpp: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 lib.py: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 libs1.cpp: - expected: {1: WA, 2: OK} + expected: + 1: {points: 0, status: WA} + 2: {points: 50, status: OK} points: 50 diff --git a/tests/packages/lim/config.yml b/tests/packages/lim/config.yml index 13000ae9..d4c2f1bc 100644 --- a/tests/packages/lim/config.yml +++ b/tests/packages/lim/config.yml @@ -1,27 +1,31 @@ title: Package with `time_limits` and `memory_limits` set - memory_limit: 15000 memory_limits: 2: 60000 - time_limit: 1000 time_limits: 1: 3000 - scores: 1: 50 2: 50 - sinol_expected_scores: lim.cpp: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 lim2.cpp: - expected: {1: OK, 2: TL} + expected: + 1: {points: 50, status: OK} + 2: {points: 0, status: TL} points: 50 lim3.cpp: - expected: {1: ML, 2: OK} + expected: + 1: {points: 0, status: ML} + 2: {points: 50, status: OK} points: 50 lim4.cpp: - expected: {1: ML, 2: OK} + expected: + 1: {points: 0, status: ML} + 2: {points: 50, status: OK} points: 50 diff --git a/tests/packages/lsa/config.yml b/tests/packages/lsa/config.yml index b24f8db7..049df992 100644 --- a/tests/packages/lsa/config.yml +++ b/tests/packages/lsa/config.yml @@ -5,15 +5,20 @@ scores: 1: 50 2: 50 extra_compilation_files: [lsalib.cpp, lsalib.h, lsalib.py] -extra_compilation_args: - cpp: lsalib.cpp +extra_compilation_args: {cpp: lsalib.cpp} sinol_expected_scores: lsa.cpp: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 lsa.py: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 lsas1.cpp: - expected: {1: WA, 2: OK} + expected: + 1: {points: 0, status: WA} + 2: {points: 50, status: OK} points: 50 diff --git a/tests/packages/ovl/config.yml b/tests/packages/ovl/config.yml index 95deefb8..99ee09c9 100644 --- a/tests/packages/ovl/config.yml +++ b/tests/packages/ovl/config.yml @@ -1,14 +1,13 @@ title: Package with `override_limits` set - memory_limit: 8192 time_limit: 100 - override_limits: cpp: memory_limit: 16384 time_limit: 10000 - sinol_expected_scores: ovl.cpp: - expected: {1: OK, 2: OK} + expected: + 1: {points: 50, status: OK} + 2: {points: 50, status: OK} points: 100 diff --git a/tests/packages/stc/config.yml b/tests/packages/stc/config.yml index f387bf41..fc312042 100644 --- a/tests/packages/stc/config.yml +++ b/tests/packages/stc/config.yml @@ -1,9 +1,8 @@ title: Package for testing if changing stack size works - memory_limit: 1000 time_limit: 10000 - sinol_expected_scores: stc.cpp: - expected: {1: OK} + expected: + 1: {points: 100, status: OK} points: 100 diff --git a/tests/packages/undocumented_options/config.yml b/tests/packages/undocumented_options/config.yml index 95aad60b..2387c6d5 100644 --- a/tests/packages/undocumented_options/config.yml +++ b/tests/packages/undocumented_options/config.yml @@ -1,18 +1,17 @@ title: Package with undocumented sinol-make options sinol_task_id: und - sinol_undocumented_time_tool: time sinol_undocumented_test_limits: true - memory_limit: 20480 time_limit: 1000 time_limits: 1a: 5000 - sinol_expected_scores: und.cpp: - expected: {1: OK} + expected: + 1: {points: 100, status: OK} points: 100 und1.cpp: - expected: {1: OK} + expected: + 1: {points: 100, status: OK} points: 100 diff --git a/tests/packages/vso/config.yml b/tests/packages/vso/config.yml index cc2bfdd8..1f9c716a 100644 --- a/tests/packages/vso/config.yml +++ b/tests/packages/vso/config.yml @@ -1,32 +1,38 @@ title: Test package for veryfing correct status order memory_limit: 16000 time_limit: 1000 - scores: 1: 100 - sinol_expected_scores: vso.cpp: + expected: + 1: {points: 100, status: OK} points: 100 - expected: {1: "OK"} vso1.cpp: + expected: + 1: {points: 0, status: WA} points: 0 - expected: {1: "WA"} vso2.cpp: + expected: + 1: {points: 0, status: RE} points: 0 - expected: {1: "RE"} vso3.cpp: + expected: + 1: {points: 0, status: ML} points: 0 - expected: {1: "ML"} vso4.cpp: + expected: + 1: {points: 0, status: TL} points: 0 - expected: {1: "TL"} vso5.cpp: + expected: + 1: {points: 0, status: RE} points: 0 - expected: {1: "RE"} vso6.cpp: + expected: + 1: {points: 0, status: ML} points: 0 - expected: {1: "ML"} vso7.cpp: + expected: + 1: {points: 0, status: TL} points: 0 - expected: {1: "TL"} diff --git a/tests/packages/wcf/config.yml b/tests/packages/wcf/config.yml index 05be7e6b..39945cf5 100644 --- a/tests/packages/wcf/config.yml +++ b/tests/packages/wcf/config.yml @@ -1,11 +1,10 @@ title: Package for testing --weak-compilation-flags memory_limit: 16000 time_limit: 1000 - scores: 1: 100 - sinol_expected_scores: wcf.cpp: - expected: {1: OK} + expected: + 1: {points: 100, status: OK} points: 100 diff --git a/tests/util.py b/tests/util.py index 83c86bd6..3034c9dd 100644 --- a/tests/util.py +++ b/tests/util.py @@ -115,6 +115,13 @@ def get_example_tests_package_path(): return os.path.join(os.path.dirname(__file__), "packages", "example_tests") +def get_icpc_package_path(): + """ + Get path to package with icpc contest type (/tests/packages/icpc) + """ + return os.path.join(os.path.dirname(__file__), "packages", "icpc") + + def create_ins(package_path, task_id): """ Create .in files for package. diff --git a/tests/version_changes/__init__.py b/tests/version_changes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/version_changes/test_expected_scores_format_change.py b/tests/version_changes/test_expected_scores_format_change.py new file mode 100644 index 00000000..9a721807 --- /dev/null +++ b/tests/version_changes/test_expected_scores_format_change.py @@ -0,0 +1,31 @@ +import pytest +import yaml + +from tests import util +from tests.fixtures import create_package +from sinol_make.util import make_version_changes +import sinol_make + + +@pytest.mark.parametrize("create_package", [util.get_simple_package_path()], indirect=True) +def test_version_change(create_package): + orig_version = sinol_make.__version__ + sinol_make.__version__ = "1.5.9" + + with open("config.yml", "r") as config_file: + config = yaml.load(config_file, Loader=yaml.FullLoader) + old_expected_scores = config["sinol_expected_scores"] + config["sinol_expected_scores"] = {'abc.cpp': {'points': 100, 'expected': {1: 'OK', 2: 'OK', 3: 'OK', 4: 'OK'}}, + 'abc1.cpp': {'points': 75, 'expected': {1: 'OK', 2: 'OK', 3: 'OK', 4: 'WA'}}, + 'abc2.cpp': {'points': 25, 'expected': {1: 'OK', 2: 'WA', 3: 'WA', 4: 'TL'}}, + 'abc3.cpp': {'points': 25, 'expected': {1: 'OK', 2: 'WA', 3: 'WA', 4: 'ML'}}, + 'abc4.cpp': {'points': 50, 'expected': {1: 'OK', 2: 'OK', 3: 'WA', 4: 'RE'}}} + with open("config.yml", "w") as config_file: + yaml.dump(config, config_file) + + make_version_changes() + + with open("config.yml", "r") as config_file: + config = yaml.load(config_file, Loader=yaml.FullLoader) + assert config["sinol_expected_scores"] == old_expected_scores + sinol_make.__version__ = orig_version From b70cfa2639709d6fd76971d271ce0018570a15a9 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 24 Sep 2023 12:32:32 +0200 Subject: [PATCH 5/5] Bump version for release (#130) --- src/sinol_make/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index 15a5b656..e7d33398 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -9,7 +9,7 @@ from sinol_make import util, oiejq -__version__ = "1.5.8" +__version__ = "1.5.9" def configure_parsers():