From 8151dba09d6074a22902061c5f1b416ed438987c Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 15:11:30 +0200 Subject: [PATCH 1/7] Allow running `sinol-make` in subdirectory of package --- src/sinol_make/util.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index 68c2f4f9..6940520d 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -33,11 +33,13 @@ def check_if_package(): """ Function to check if current directory is a package """ - - cwd = os.getcwd() - if os.path.exists(os.path.join(cwd, 'config.yml')): + if os.path.exists(os.path.join(os.getcwd(), 'config.yml')): + return True + elif os.path.exists(os.path.join(os.getcwd(), '..', 'config.yml')): + os.chdir('..') return True - return False + else: + return False def exit_if_not_package(): From 902abc464805d3d0b7c50cc9218be64ca0a48132 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 15:12:04 +0200 Subject: [PATCH 2/7] Add test --- tests/commands/run/test_integration.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index c9837a8c..f7a599e1 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -553,3 +553,17 @@ 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()], indirect=True) +def test_cwd_in_prog(create_package): + """ + Test if `sinol-make` works when cwd is in prog. + """ + package_path = create_package + os.chdir("prog") + create_ins_outs(package_path) + parser = configure_parsers() + args = parser.parse_args(["run"]) + command = Command() + command.run(args) From 6b9f8660d6ee7b7abd64698bcc6ac545ed0db7f7 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 15:13:04 +0200 Subject: [PATCH 3/7] Revert "Add test" This reverts commit 902abc464805d3d0b7c50cc9218be64ca0a48132. --- tests/commands/run/test_integration.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index f7a599e1..c9837a8c 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -553,17 +553,3 @@ 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()], indirect=True) -def test_cwd_in_prog(create_package): - """ - Test if `sinol-make` works when cwd is in prog. - """ - package_path = create_package - os.chdir("prog") - create_ins_outs(package_path) - parser = configure_parsers() - args = parser.parse_args(["run"]) - command = Command() - command.run(args) From c268ea12b003373bfac50e0a77ab622d94e9f787 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 15:13:04 +0200 Subject: [PATCH 4/7] Revert "Allow running `sinol-make` in subdirectory of package" This reverts commit 8151dba09d6074a22902061c5f1b416ed438987c. --- src/sinol_make/util.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sinol_make/util.py b/src/sinol_make/util.py index 6940520d..68c2f4f9 100644 --- a/src/sinol_make/util.py +++ b/src/sinol_make/util.py @@ -33,13 +33,11 @@ def check_if_package(): """ Function to check if current directory is a package """ - if os.path.exists(os.path.join(os.getcwd(), 'config.yml')): - return True - elif os.path.exists(os.path.join(os.getcwd(), '..', 'config.yml')): - os.chdir('..') + + cwd = os.getcwd() + if os.path.exists(os.path.join(cwd, 'config.yml')): return True - else: - return False + return False def exit_if_not_package(): From 0a9d6dfe3115a7c057cc78574128427e184b7620 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sat, 23 Sep 2023 22:24:03 +0200 Subject: [PATCH 5/7] 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 6/7] 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 7/7] 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)