diff --git a/TestGenerator/TestGenerator b/TestGenerator/TestGenerator new file mode 100644 index 0000000..882882b Binary files /dev/null and b/TestGenerator/TestGenerator differ diff --git a/TestGenerator/TestGenerator.exe b/TestGenerator/TestGenerator.exe new file mode 100644 index 0000000..9193dde Binary files /dev/null and b/TestGenerator/TestGenerator.exe differ diff --git a/TestGenerator/caseHandler.c b/TestGenerator/caseHandler.c new file mode 100644 index 0000000..34fbb4b --- /dev/null +++ b/TestGenerator/caseHandler.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include "constants.h" +#include "error.h" +#include "caseHandler.h" + +int caseHandler_GetNextCase(char* programNumber) { + int numbers[3]; + if(sscanf(programNumber, "%d-%d-%d", &numbers[0], &numbers[1], &numbers[2]) != 3) + error("Invalid program number: %s\n", programNumber); + + // Get the name of the directory + char dirName[MAX_PROGRAM_PATH_SIZE]; + snprintf(dirName, MAX_PROGRAM_PATH_SIZE, "%s/tests/%02d-%02d-%02d/", + BASE_PATH, numbers[0], numbers[1], numbers[2]); + + // Find what the first unwritten test number is + int number = 1; + char testName[MAX_PROGRAM_PATH_SIZE+10]; + while(1) { + snprintf(testName, MAX_PROGRAM_PATH_SIZE+10, "%s1-%02d.in", dirName, number); + FILE* file = fopen(testName, "r"); + if(file) { + fclose(file); + number++; + continue; + } + break; + } + + return number; +} + +void caseHandler_PopulateNextCase(char* programNumber, int caseNumber, char** programIO) { + int numbers[3]; + if(sscanf(programNumber, "%d-%d-%d", &numbers[0], &numbers[1], &numbers[2]) != 3) + error("Invalid program number: %s\n", programNumber); + + // Open up the input and output files + char inFileName[MAX_PROGRAM_PATH_SIZE], outFileName[MAX_PROGRAM_PATH_SIZE]; + snprintf(inFileName, MAX_PROGRAM_PATH_SIZE, "%s/tests/%02d-%02d-%02d/1-%02d.in", + BASE_PATH, numbers[0], numbers[1], numbers[2], caseNumber); + snprintf(outFileName, MAX_PROGRAM_PATH_SIZE, "%s/tests/%02d-%02d-%02d/1-%02d.out", + BASE_PATH, numbers[0], numbers[1], numbers[2], caseNumber); + FILE* inFile = fopen(inFileName, "w"); + if(!inFile) error("Failed to write to .in file\n"); + FILE* outFile = fopen(outFileName, "w"); + if(!outFile) error("Failed to write to .out file\n"); + + // Turn all "\r\n" occurrences in the output into just '\n' + char* newOutput = calloc(strlen(programIO[1])+1, 1); + if(!newOutput) error("Failed to allocate new output buffer\n"); + int i = 0, index = 0; + while(programIO[1][i]) { + if(programIO[1][i] == '\r' && programIO[1][i+1] == '\n') { + newOutput[index++] = '\n'; + i += 2; + } else newOutput[index++] = programIO[1][i++]; + } + free(programIO[1]); + programIO[1] = newOutput; + + // Strip the '\n' at the end of the input and output + programIO[0][strlen(programIO[0])-1] = 0; + programIO[1][strlen(programIO[1])-1] = 0; + + // Write the input and output from programIO to the files + fwrite(programIO[0], 1, strlen(programIO[0]), inFile); + fwrite(programIO[1], 1, strlen(programIO[1]), outFile); + + // Close the files + fclose(inFile); + fclose(outFile); +} \ No newline at end of file diff --git a/TestGenerator/caseHandler.h b/TestGenerator/caseHandler.h new file mode 100644 index 0000000..4637959 --- /dev/null +++ b/TestGenerator/caseHandler.h @@ -0,0 +1,20 @@ +#ifndef CASEHANDLER_H +#define CASEHANDLER_H + +/** + * \brief Gets the next case yet to be populated. For example, if 1-01.in and 1-02.in + * existed, this function would return 3 because 1-03.in is the next case to fill + * \param programNumber The program number formatted as "--" + * \return The next case yet to be filled + */ +int caseHandler_GetNextCase(char* programNumber); + +/** + * \brief Populates a case's .in and .out files + * \param programNumber The program number formatted as "--" + * \param caseNumber The case number to populate + * \param programIO The IO gathered from the generator program + */ +void caseHandler_PopulateNextCase(char* programNumber, int caseNumber, char** programIO); + +#endif \ No newline at end of file diff --git a/TestGenerator/constants.c b/TestGenerator/constants.c new file mode 100644 index 0000000..b5a63ea --- /dev/null +++ b/TestGenerator/constants.c @@ -0,0 +1,38 @@ +#include +#include +#include +#ifdef WIN32 +#include +#define PATH_SEPARATOR '\\' +#else +#include +#include +#define PATH_SEPARATOR '/' +#endif +#include "error.h" +#include "constants.h" + +char BASE_PATH[MAX_PROGRAM_PATH_SIZE]; + +void constants_GetBasePath(void) { + char exePath[MAX_PROGRAM_PATH_SIZE]; + + #ifdef WIN32 + DWORD length = GetModuleFileNameA(NULL, exePath, MAX_PROGRAM_PATH_SIZE); + if(!length || length == MAX_PROGRAM_PATH_SIZE) + error("Failed to get the executable's path\n"); + #else + int length = readlink("/proc/self/exe", exePath, MAX_PROGRAM_PATH_SIZE-1); + if(length == -1) + error("Readlink failed\n"); + exePath[length] = 0; + #endif + + // Remove the executable's name and go up one directory + char* lastSep = strrchr(exePath, PATH_SEPARATOR); + if(lastSep) *lastSep = 0; + lastSep = strrchr(exePath, PATH_SEPARATOR); + if(lastSep) *lastSep = 0; + + strncpy(BASE_PATH, exePath, MAX_PROGRAM_PATH_SIZE-1); +} \ No newline at end of file diff --git a/TestGenerator/constants.h b/TestGenerator/constants.h new file mode 100644 index 0000000..0b3064b --- /dev/null +++ b/TestGenerator/constants.h @@ -0,0 +1,21 @@ +#ifndef CONSTANTS_H +#define CONSTANTS_H + +#define MAX_PROGRAM_PATH_SIZE 2048 + +#define INPUT_BUFFER_SIZE 4096 + +#define PROGRAMIO_INITIAL_SIZE 4096 +#define PROGRAMIO_STEP_SIZE 4096 + +// In milliseconds +#define OUTPUT_DELAY 100 + +// Fill this in yourself. Only necessary on Windows +#define C_CODE_PATH "" + +// Populated at the beginning of runtime +extern char BASE_PATH[]; +void constants_GetBasePath(void); + +#endif diff --git a/TestGenerator/error.c b/TestGenerator/error.c new file mode 100644 index 0000000..c35f05f --- /dev/null +++ b/TestGenerator/error.c @@ -0,0 +1,21 @@ +#include +#include +#include +#include "constants.h" +#include "error.h" + +_Noreturn void __error(int lineNumber, const char* fileName, const char* format, ...) { + if(!format) exit(0); + + #ifdef WIN32 + fprintf(stderr, "Error at %s:%d: ", fileName+strlen(C_CODE_PATH), lineNumber); + #else + fprintf(stderr, "Error at %s:%d: ", fileName, lineNumber); + #endif + va_list argv; + va_start(argv, format); + vfprintf(stderr, format, argv); + va_end(argv); + + exit(1); +} \ No newline at end of file diff --git a/TestGenerator/error.h b/TestGenerator/error.h new file mode 100644 index 0000000..b9118c1 --- /dev/null +++ b/TestGenerator/error.h @@ -0,0 +1,14 @@ +#ifndef ERROR_H +#define ERROR_H + +#include + +_Noreturn void __error(int lineNumber, const char* fileName, const char* format, ...); + +/** + * \brief Prints out an error message and exits the program. error(NULL) to exit normally. + * Takes in arguments just like printf and prints them to stderr before exiting + */ +#define error(___format, ...) __error(__LINE__, __FILE__, ___format, ##__VA_ARGS__) + +#endif \ No newline at end of file diff --git a/TestGenerator/main.c b/TestGenerator/main.c new file mode 100644 index 0000000..5f298bc --- /dev/null +++ b/TestGenerator/main.c @@ -0,0 +1,31 @@ +#include +#include +#include "programHandler.h" +#include "caseHandler.h" +#include "constants.h" + +#include +int main(int argc, char** argv) { + if(argc != 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + // Get the base path + constants_GetBasePath(); + + // Run the program and get the input/output + char* name = programHandler_GetProgramName(argv[1]); + char** programIO = programHandler_RunPythonProgram(name); + free(name); + + // Write the test case + int nextCase = caseHandler_GetNextCase(argv[1]); + caseHandler_PopulateNextCase(argv[1], nextCase, programIO); + + free(programIO[0]); + free(programIO[1]); + free(programIO); + + printf("Wrote test %d!\n", nextCase); +} diff --git a/TestGenerator/programHandler.c b/TestGenerator/programHandler.c new file mode 100644 index 0000000..a87cbe9 --- /dev/null +++ b/TestGenerator/programHandler.c @@ -0,0 +1,298 @@ +#include +#include +#include +#include +#include +#ifdef WIN32 +#include +#else +#include +#include +#endif +#include "constants.h" +#include "error.h" +#include "programHandler.h" + +char* programHandler_GetProgramName(char* programNumber) { + int numbers[3]; + if(sscanf(programNumber, "%d-%d-%d", &numbers[0], &numbers[1], &numbers[2]) != 3) + error("Invalid program number: %s\n", programNumber); + + // Get the name of the program + char raw[MAX_PROGRAM_PATH_SIZE]; + if(!raw) error("Failed to create a buffer to store the program path\n"); + snprintf(raw, MAX_PROGRAM_PATH_SIZE, + "%s/tests/%02d-%02d-%02d/generator.py", + BASE_PATH, numbers[0], numbers[1], numbers[2]); + + FILE* generator = fopen(raw, "r"); + if(!generator) error("Failed to find the generator at %s\n", raw); + fclose(generator); + + // Condense the memory used + char* ret = calloc(strlen(raw)+1, 1); + if(!ret) error("Failed to create a buffer to store the program path\n"); + strcpy(ret, raw); + + return ret; +} + +#ifdef WIN32 +void programHandler_ReadAvailableOutput(HANDLE hOutput, char **outputBuffer, unsigned *currentSize) { + DWORD availableBytes = 0; + char tempBuffer[256]; + DWORD bytesRead; + + while(PeekNamedPipe(hOutput, NULL, 0, NULL, &availableBytes, NULL) && availableBytes > 0) { + if(ReadFile(hOutput, tempBuffer, sizeof(tempBuffer) - 1, &bytesRead, NULL) && bytesRead > 0) { + tempBuffer[bytesRead] = 0; + + size_t newSize = *currentSize + bytesRead + 1; + if(newSize >= PROGRAMIO_INITIAL_SIZE) { + char *temp = realloc(*outputBuffer, newSize); + if(!temp) error("Failed to reallocate output buffer"); + *outputBuffer = temp; + } + + strcat(*outputBuffer, tempBuffer); + *currentSize += bytesRead; + } else break; + } +} + +int programHandler_ProcessHasExited(HANDLE processHandle) { + DWORD exitCode = 0; + if(!GetExitCodeProcess(processHandle, &exitCode)) + error("Failed to get process exit code"); + return exitCode != STILL_ACTIVE; +} + + +char** programHandler_RunPythonProgram(char* path) { + HANDLE hChildStdoutRd = NULL, hChildStdoutWr = NULL; + HANDLE hChildStdinRd = NULL, hChildStdinWr = NULL; + SECURITY_ATTRIBUTES saAttr = {0}; + PROCESS_INFORMATION piProcInfo = {0}; + STARTUPINFO siStartInfo = {0}; + char **programIO = calloc(2, sizeof(char*)); + if(!programIO) error("Failed to allocate memory for program IO"); + + programIO[0] = calloc(PROGRAMIO_INITIAL_SIZE, 1); + programIO[1] = calloc(PROGRAMIO_INITIAL_SIZE, 1); + if(!programIO[0] || !programIO[1]) { + free(programIO); + error("Failed to allocate input/output buffer"); + } + + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = 1; + saAttr.lpSecurityDescriptor = NULL; + + if(!CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("Stdout pipe creation failed"); + } + + if(!CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("Stdin pipe creation failed"); + } + + if(!SetHandleInformation(hChildStdoutRd, HANDLE_FLAG_INHERIT, 0)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("SetHandleInformation failed"); + } + + siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.hStdError = hChildStdoutWr; + siStartInfo.hStdOutput = hChildStdoutWr; + siStartInfo.hStdInput = hChildStdinRd; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; + + char processCommand[MAX_PROGRAM_PATH_SIZE + 20]; + if(snprintf(processCommand, sizeof(processCommand), "python -u \"%s\"", path) >= sizeof(processCommand)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("Python path too long"); + } + + if(!CreateProcess(NULL, processCommand, NULL, NULL, 1, 0, NULL, NULL, &siStartInfo, &piProcInfo)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("CreateProcess failed"); + } + + // Close unused handles + CloseHandle(hChildStdoutWr); + CloseHandle(hChildStdinRd); + + // Give python some time to crash if the program raises an error + Sleep(OUTPUT_DELAY); + + // If it didn't crash, print the input prompt + if(!programHandler_ProcessHasExited(piProcInfo.hProcess)) + printf("Case generator started. Input below:\n"); + + char inputBuffer[INPUT_BUFFER_SIZE]; + DWORD bytesWritten; + unsigned inputSize = 0; + unsigned outputSize = 0; + + while(!programHandler_ProcessHasExited(piProcInfo.hProcess)) { + if(fgets(inputBuffer, INPUT_BUFFER_SIZE, stdin)) { + unsigned len = strlen(inputBuffer); + + unsigned newInputSize = inputSize + len + 1; + if(newInputSize >= PROGRAMIO_INITIAL_SIZE) { + char *tempInput = realloc(programIO[0], newInputSize); + if(!tempInput) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("Failed to reallocate input buffer"); + } + programIO[0] = tempInput; + } + + strcat(programIO[0], inputBuffer); + inputSize += len; + + if(!WriteFile(hChildStdinWr, inputBuffer, (DWORD)len, &bytesWritten, NULL)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("Failed to write to stdin pipe"); + } + + // After writing input, read any available output + programHandler_ReadAvailableOutput(hChildStdoutRd, &programIO[1], &outputSize); + } + + // Also poll output while waiting for next input + programHandler_ReadAvailableOutput(hChildStdoutRd, &programIO[1], &outputSize); + Sleep(OUTPUT_DELAY); + } + + // Final read to collect remaining output + programHandler_ReadAvailableOutput(hChildStdoutRd, &programIO[1], &outputSize); + + // Check that the program exited correctly (exit code 0) + DWORD exitCode = 1; + if(!GetExitCodeProcess(piProcInfo.hProcess, &exitCode)) { + free(programIO[0]); + free(programIO[1]); + free(programIO); + error("Failed to get process exit code"); + } + if(exitCode) + error("Python exited with code %u. Full output:\n%s", exitCode, programIO[1]); // Leaks memory, but I don't care, I'd rather the logs + + CloseHandle(hChildStdinWr); + CloseHandle(hChildStdoutRd); + WaitForSingleObject(piProcInfo.hProcess, INFINITE); + CloseHandle(piProcInfo.hProcess); + CloseHandle(piProcInfo.hThread); + + return programIO; +} +#else +char** programHandler_RunPythonProgram(char* path) { + int inPipe[2], outPipe[2]; + pid_t pid; + char** programIO = calloc(2, sizeof(char*)); + + if(!programIO) error("Failed to allocate memory for program IO"); + programIO[0] = calloc(PROGRAMIO_INITIAL_SIZE, 1); + programIO[1] = calloc(PROGRAMIO_INITIAL_SIZE, 1); + if(!programIO[0] || !programIO[1]) error("Failed to allocate input/output buffer"); + + if(pipe(inPipe) == -1 || pipe(outPipe) == -1) + error("Pipe creation failed"); + + if((pid = fork()) == -1) + error("Fork failed"); + + if(!pid) { + // Redirect stdio and stdout + dup2(inPipe[0], STDIN_FILENO); + dup2(outPipe[1], STDOUT_FILENO); + close(inPipe[0]); + close(inPipe[1]); + close(outPipe[0]); + close(outPipe[1]); + + execlp("python3", "python3", "-u", path, NULL); + error("Failed to exec Python"); + } else { + close(inPipe[0]); + close(outPipe[1]); + + fcntl(outPipe[0], F_SETFL, O_NONBLOCK); + + // Handle IO + char inputBuffer[INPUT_BUFFER_SIZE]; + int inputSize = 0; + while(1) { + if(fgets(inputBuffer, INPUT_BUFFER_SIZE, stdin)) { + int len = strlen(inputBuffer); + + // Store input dynamically + if(inputSize + len >= PROGRAMIO_INITIAL_SIZE) { + programIO[0] = realloc(programIO[0], inputSize + len + 1); + if(!programIO[0]) error("Failed to reallocate input buffer"); + } + + strcat(programIO[0], inputBuffer); + inputSize += len; + + // Write input to Python process + write(inPipe[1], inputBuffer, len); + } + + // Check if Python has produced output + usleep(OUTPUT_DELAY * 1000); + char buffer[256]; + int bytesRead = read(outPipe[0], buffer, sizeof(buffer) - 1); + + if(bytesRead > 0) { + buffer[bytesRead] = 0; + + // Store output dynamically + size_t totalSize = strlen(programIO[1]); + programIO[1] = realloc(programIO[1], totalSize + bytesRead + 1); + if (!programIO[1]) error("Failed to reallocate output buffer"); + + strcat(programIO[1], buffer); + break; + } + } + close(inPipe[1]); + + // Read remaining output from Python + char buffer[256]; + ssize_t bytesRead; + while((bytesRead = read(outPipe[0], buffer, sizeof(buffer) - 1)) > 0) { + buffer[bytesRead] = 0; + + // Store output dynamically + size_t totalSize = strlen(programIO[1]); + programIO[1] = realloc(programIO[1], totalSize + bytesRead + 1); + if (!programIO[1]) error("Failed to reallocate output buffer"); + + strcat(programIO[1], buffer); + } + + close(outPipe[0]); + waitpid(pid, NULL, 0); + } + return programIO; +} +#endif \ No newline at end of file diff --git a/TestGenerator/programHandler.h b/TestGenerator/programHandler.h new file mode 100644 index 0000000..8546a45 --- /dev/null +++ b/TestGenerator/programHandler.h @@ -0,0 +1,18 @@ +#ifndef PROGRAMHANDLER_H +#define PROGRAMHANDLER_H + +/** + * \brief Gets the program name based on the program number + * \param programNumber The program number formatted "--" + * \return The path to the generator program to run + */ +char* programHandler_GetProgramName(char* programNumber); + +/** + * \brief Runs the generator program from \c programHandler_getProgramName and returns its IO + * \param path The path to the test case generator python program + * \return The generator's IO + */ +char** programHandler_RunPythonProgram(char* path); + +#endif \ No newline at end of file diff --git a/TestGenerator/readme.md b/TestGenerator/readme.md new file mode 100644 index 0000000..ddd1049 --- /dev/null +++ b/TestGenerator/readme.md @@ -0,0 +1,15 @@ +# Test Generator + +## Usage + +Just create a generator.py file in the test folder you want to write tests for +Run the program with the test ID and let it handle everything for you \ +For example, to generate tests for program 01-06-01, run "[./]TestGenerator 01-06-01" +and follow the steps from there + +## Modifying + +The source code for the TestGenerator is all within this directory. Just be sure +to modify **C_CODE_PATH** in [constants.h](constants.h) if you are on Windows or errors won't print properly + +I've compiled using GCC on both windows and linux, but everything used here should be standard \ No newline at end of file diff --git a/tests/01-06-01/generator.py b/tests/01-06-01/generator.py new file mode 100644 index 0000000..4bf588e --- /dev/null +++ b/tests/01-06-01/generator.py @@ -0,0 +1,4 @@ +letter = input() +print(f"{letter}'s code point is {ord(letter)}") +print(f"before {letter} is {chr(ord(letter)-1)}, code point {ord(letter)-1}") +print(f"after {letter} is {chr(ord(letter)+1)}, code point {ord(letter)+1}") \ No newline at end of file diff --git a/tests/01-06-03/generator.py b/tests/01-06-03/generator.py new file mode 100644 index 0000000..0114b25 --- /dev/null +++ b/tests/01-06-03/generator.py @@ -0,0 +1,4 @@ +sentence = input() +word = input() +space = int(input()) +print(f"{sentence[:space]}{word}{sentence[space:]}") \ No newline at end of file diff --git a/tests/02-04-01/generator.py b/tests/02-04-01/generator.py new file mode 100644 index 0000000..88243d5 --- /dev/null +++ b/tests/02-04-01/generator.py @@ -0,0 +1 @@ +print(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][int(input())-1]) \ No newline at end of file diff --git a/tests/02-04-02/generator.py b/tests/02-04-02/generator.py new file mode 100644 index 0000000..8f9a697 --- /dev/null +++ b/tests/02-04-02/generator.py @@ -0,0 +1,7 @@ +num = int(input()) +if num > 0: + print("positive") +elif num < 0: + print("negative") +else: + print("zero") \ No newline at end of file diff --git a/tests/02-04-03/generator.py b/tests/02-04-03/generator.py new file mode 100644 index 0000000..2744a82 --- /dev/null +++ b/tests/02-04-03/generator.py @@ -0,0 +1 @@ +print(max([int(input()) for _ in range(3)])) \ No newline at end of file diff --git a/tests/02-04-04/generator.py b/tests/02-04-04/generator.py new file mode 100644 index 0000000..bac30bc --- /dev/null +++ b/tests/02-04-04/generator.py @@ -0,0 +1,6 @@ +number = int(input()) +if not number % 3: + print("Fizz Buzz" if not number % 5 else "Fizz") +elif not number % 5: + print("Buzz") +else: print(number) \ No newline at end of file diff --git a/tests/02-04-05/generator.py b/tests/02-04-05/generator.py new file mode 100644 index 0000000..2aa7214 --- /dev/null +++ b/tests/02-04-05/generator.py @@ -0,0 +1 @@ +print(sum([int(input()) for _ in range(3)]) / 3 >= 80) \ No newline at end of file diff --git a/tests/03-06-02/generator.py b/tests/03-06-02/generator.py new file mode 100644 index 0000000..69c2f95 --- /dev/null +++ b/tests/03-06-02/generator.py @@ -0,0 +1,20 @@ +msg = input() +keys = [int(input()) for _ in range(3)] + +new_msg = "" + +index = 0 +for i in range(len(msg)): + if msg[i] == " ": + new_msg += " " + continue + if msg[i].isupper(): + new_msg += chr((ord(msg[i]) - ord('A') + keys[index%3]) % 26 + ord('A')) + index += 1 + elif msg[i].islower(): + new_msg += chr((ord(msg[i]) - ord('a') + keys[index%3]) % 26 + ord('a')) + index += 1 + else: + new_msg += msg[i] + +print(new_msg) \ No newline at end of file