// monad // For illinois.edu CS 225 spring 2011 // By Jack Toole #include "monad.h" namespace monad { void find_base_dir(const char * argv0); void processArgs(int argc, const char * const * argv); void copyRequiredFiles(); void getLibs(const vector<string> & libs); string updateFolder(const string & folder, bool link); string getFolder(const string & folder, bool link); void importFiles(const string & preservedFolder, const string & theSourceFolder, const string & destFolder, const vector<string> & files); void exec_command(const string & command); string assignment_base; int8_t mp_part; const int8_t no_mp_part = -1; string testsFolder; string sourceFolder; string gradeFolder; string tempFolder; FileMap config; namespace opts { bool solution = false; bool clean = true; bool update = true; bool staff = false; bool newtests = false; bool provided = false; bool verbose = false; bool valgrind = false; bool parallel = false; bool help = false; bool info = false; bool license = false; #if OPTIMIZE bool optimize = true; #else bool optimize = false; #endif } } string kyleGetDate() { string dateAndTime = "Current Date and Time: "; time_t currentTime; struct tm * timeStructure; time(¤tTime); timeStructure = localtime(¤tTime); dateAndTime += asctime(timeStructure); return dateAndTime; } int main(int argc, const char * const * argv) { using namespace monad; output::set_error_message(); // Find monad/ directory find_base_dir(argv[0]); // Read in local config settings. // Necessary to do this early for [SVN Root] url readConfig("./", config); processArgs(argc, argv); cout << versioninfo::official_name << endl; cout << "Testing " << assignment_base; if (mp_part != no_mp_part) cout << '.' << (int)mp_part; cout << "..." << endl; cout << getRandomQuote() << endl; cout << endl; cout << "Setting up test environment..." << endl; // Read in test-specific config settings if (mp_part == no_mp_part) readConfig(testsFolder, config); else readConfig(testsFolder, config, to_string((int)mp_part)); copyRequiredFiles(); // Sleep for a second to avoid clock skew warnings // This cummulatively adds about 5 minutes to grade each mp, // but with the benefit of avoiding newsgroup posts // CHANGED: Judging by previous emails, the time needed for // this would vary significantly. not sure about solution // sleep(1); output::header("Compiling"); chdir(gradeFolder.c_str()); // #define MP_PART_NUMBER in proxy ofstream mp_part_file; mp_part_file.open("_mp_part_number.h"); if (mp_part == no_mp_part) mp_part_file << "#define MP_PART_NUMBER NO_MP_PART" << endl; else mp_part_file << "#define MP_PART_NUMBER " << (int)mp_part << endl; mp_part_file.close(); // run [Pre-Make Commands] config header const vector<string> & processing_commands = config["Pre-Make Commands"]; for (size_t i = 0; i < processing_commands.size(); i++) exec_command(processing_commands[i]); string makestr = "/usr/bin/make --warn-undefined-variables -Wfatal-errors"; if (opts::parallel) makestr += " --jobs=4"; if (!opts::verbose) makestr += " --quiet"; if (opts::optimize) makestr += " OPTIMIZE=on"; if (!config["Make Options"].empty()) makestr += " " + config["Make Options"][0]; if (opts::verbose) cout << makestr <<endl; // Compile with make system(makestr.c_str()); // yes, system is insecure if the user has control // over config.ini. But students don't. // TODO (toole1): Yeah but this leaves us open to // aliasing issues, or forces us to specify make's // path. Ugly either way. // TODO exec("make", "--quiet", "--warn-undefined-variables", cout << endl << endl; const char * verboseflag = (opts::verbose ? "--verbose" : NULL); const char * valgrindflag = (opts::valgrind ? "--valgrind" : NULL); int score = exec("./proxy", verboseflag, valgrindflag); // TODO (toole1): this causes weird output when scores are like 200 if (score < 0) { output::header("Running tests"); cout << "Could not execute test cases" << endl << endl; score = 0; cout << endl; output::total_score(score, -1); } return score; } void monad::processArgs(int argc, const char * const * argv) { // Create OptionsMap for options and vector for positional arguments: OptionsParser options; // Add our possible options to our map options.addOption("solution", opts::solution); options.addOption("newtests", opts::newtests); options.addOption("provided", opts::provided); options.addOption("clean", opts::clean); options.addOption("update", opts::update); options.addOption("staff", opts::staff); options.addOption("optimize", opts::optimize); options.addOption("verbose", opts::verbose); options.addOption("valgrind", opts::valgrind); options.addOption("parallel", opts::parallel); options.addOption("help", opts::help); options.addOption("h", opts::help); options.addOption("info", opts::info); options.addOption("version", opts::info); options.addOption("v", opts::info); options.addOption("license", opts::license); // Add arguments string assignment = ""; options.addArg(assignment); // Read in options and arguments vector<string> posargs = options.parse(argc, argv); // Help if (opts::help || toLower(assignment) == "help") { if (toLower(assignment) == "config") printHelpConfig(); else if (toLower(assignment) == "tests") printHelpTests(); else printHelp(); exit(0); } // Info if (opts::info) { printInfo(); exit(0); } // License if (opts::license) { printLicense(); exit(0); } // Clean if (toLower(assignment) == "clean") { system("/bin/rm -rf *_grade/ *_tests/ *_newtests/ *_providedtests *_solution/"); exit(0); } // Check argument list length if (assignment == "") { cout << "Usage: " << argv[0] << " mp1" << endl; cout << "Run \'" << argv[0] << " --help\' for more information" << endl; exit(0); } // Find mp/lab name and tests folder vector<string> splitname = tokenize(assignment, '.'); assignment_base = splitname[0]; if (splitname.size() == 1) mp_part = no_mp_part; else mp_part = lexical_cast<int>(splitname[1].c_str()); gradeFolder = "./" + assignment_base + "_grade/"; if (!exists(gradeFolder)) opts::clean = true; if (opts::clean) tempFolder = ""; else tempFolder = "./" + assignment_base + "_temp/"; // Find source folder (i.e. ../mp1) if (opts::solution) sourceFolder = updateFolder(assignment_base + "_solution/", false); else sourceFolder = getFolder(assignment_base + '/', false); // tests folder if (opts::provided) testsFolder = updateFolder(assignment_base + "_provided/", false); else if (opts::newtests) testsFolder = updateFolder(assignment_base + "_newtests/", false); else testsFolder = updateFolder(assignment_base + "_tests/", false); } void monad::find_base_dir(const char * argv0) { EXIT_IF_ERROR(argv0 == NULL); size_t argv0len = strlen(argv0); char * dir = new char[argv0len + 1]; strncpy(dir, argv0, argv0len); dir[argv0len] = '\0'; size_t i = argv0len + 1; do { i--; if (argv0[i] == '/') break; } while (i != 0); // Change directory if (i != 0) { dir[i] = '\0'; EXIT_IF_ERROR(chdir(dir)); } delete [] dir; // Ensure the dir is correct if (!exists("./.monadid")) { cerr << "Could not find monad directory. " "Please run ./monad from the directory it is located in." << endl; exit(-1); } } void monad::copyRequiredFiles() { // Clear out the temp testing folder if (opts::clean) forceRemoveDir(gradeFolder); else { forceRemoveDir(tempFolder); EXIT_IF_ERROR(rename(gradeFolder.c_str(), tempFolder.c_str())); } exec("/bin/mkdir",gradeFolder.c_str()); // Copy and link appropriate files - parsed from config.ini importFiles(tempFolder, "./", gradeFolder, config["Monad Files"]); importFiles(tempFolder, testsFolder, gradeFolder, config["Testing Files"]); importFiles("", sourceFolder, gradeFolder, config["Required Files"]); importFiles(tempFolder, "", gradeFolder, config["Preserved Files"]); forceRemoveDir(tempFolder); getLibs(config["Libraries"]); } void monad::importFiles(const string & preservedFolder, const string & theSourceFolder, const string & destFolder, const vector<string> & files) { // 0 for student errors for missing Required Files dir int student_error_code = ((preservedFolder == "") ? 0 : -1); assertExists(destFolder); if (preservedFolder != "") assertExists(preservedFolder); if (theSourceFolder != "") assertExists(theSourceFolder, student_error_code); for (size_t i = 0; i < files.size(); i++) { string preservedFile = preservedFolder + files[i]; string destFile = destFolder + files[i]; // Move the file from it's preservation instance if (preservedFolder != "" && exists(preservedFile) && (theSourceFolder == "" || (permissions(preservedFile) & S_IWUSR) == 0)) { //!! cout << "mv " << preservedFile << ' ' << destFile << endl; EXIT_IF_ERROR(rename(preservedFile.c_str(), destFile.c_str())); } else if (theSourceFolder != "") { // copy the file from it's source string sourceFile = theSourceFolder + files[i]; assertExists(sourceFile, student_error_code); // Remove hacky call to exec here... EXIT_IF_ERROR(exec("cp", "-RL", sourceFile.c_str(), destFile.c_str()) != 0, "cp " + sourceFile + " " + destFile + " failed"); } else continue; // TODO (toole1) Protect files. This is hacky and should be in util // TODO (toole1) this should also be AFTER Pre-Make Commands // EXIT_IF_ERROR(chmod(destFile.c_str(), // S_IRUSR | (permissions(destFile) & S_IXUSR)) != 0); } } void monad::getLibs(const vector<string> & libs) { for (size_t lib_i = 0; lib_i < libs.size(); lib_i++) { string folder = updateFolder(libs[lib_i], false); protectDir(folder); chdir(gradeFolder.c_str()); system(("/bin/ln -s ../"+folder+"* ./").c_str()); chdir(".."); } } string monad::updateFolder(const string & folder, bool link) { string get = getFolder(folder, link); if (opts::update) exec(/*-1,*/ "svn","up", get.c_str()); return get; } string monad::getFolder(const string & folder, bool link) { // Look in the current folder string target = "./" + folder; if (exists(target + "/")) return target + "/"; // Look in the parent folder string source = "../" + folder; if (exists(source + "/")) { if (!link) return source + "/"; EXIT_IF_ERROR(symlink(source.c_str(), target.c_str()) != 0); return target + "/"; } // Maybe it actually *is* the parent folder string cwd = getcwdstr(); size_t cwd_i = findNthLast(cwd, '/', 2); if (cwd_i != string::npos && cwd.length() - cwd_i > folder.length() && cwd.compare(cwd_i, folder.length(), folder) == 0) { if (!link) return "../"; EXIT_IF_ERROR(symlink("../", target.c_str()) != 0); return target + "/"; } // Look two directories up and over - why not? If the parent folder is // the target source folder for the mp/lab, then the tests or libs // may be two up and over source = "../../" + folder; if (exists(source + "/")) { if (!link) return source + "/"; EXIT_IF_ERROR(symlink(source.c_str(), target.c_str()) != 0); return target + "/"; } // Check Subversion const char * svn_config_name = NULL; const char * svn_subdir = NULL; if (!opts::staff) { svn_config_name = "SVN Root"; svn_subdir = "/_public/"; } else { svn_config_name = "Staff SVN"; svn_subdir = "/_current/"; } if (!config[svn_config_name].empty()) { string svndir = config[svn_config_name][0] + svn_subdir + folder; // TODO (toole1): Won't work if user needs to type password int svnstatus = exec(/*-1,*/ "svn","co",svndir.c_str()); if (svnstatus == 0) return target + "/"; } cerr << "Error: " << folder << " not found." << endl; exit(-1); return ""; } // Execute a monad or command line command void monad::exec_command(const string & command) { vector<string> args = tokenize(command, ' '); // Allow processing of special internals if (args[0] == "rename_main") { EXIT_IF_ERROR(args.size() != 3, "rename_main must take 2 arguments: a file and a new name"); rename_main(args[1], args[2]); return; } #if DEBUG cout << "Pre-Make Command: " << command << endl; #endif system(command.c_str()); }