Skip to content
Snippets Groups Projects
monad.cpp 13.03 KiB
// monad
// For illinois.edu CS 225 spring 2011
// By Jack Toole

#include <iostream>
#include <fstream>
#include <map>
#include <sstream>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <vector>

#include "util.h"
#include "output.h"

using namespace std;
using namespace util;
using namespace output;

namespace monad
{
void find_base_dir(const char * argv0);
void printHelp();
void printInfo();
void processArgs(int argc, char ** 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 & sourceFolder,
                 const string & destFolder, const vector<string> & files);
void exec_command(const string & command);

string name;

string testsFolder;
string sourceFolder;
string gradeFolder;
string tempFolder;

FileMap    config;

int8_t mp_part;
const int8_t no_mp_part = -1;

namespace opts {
bool clean    = true;
bool update   = true;
bool staff    = false;
#if OPTIMIZE
bool optimize = true;
#else
bool optimize = false;
#endif
}

namespace version {
const char * official_name = "CS 225 Monad";
const char * version_name  = "awakening";
const char * date          = "15 July 2011";
}

}

string kyleGetDate() {
	string dateAndTime = "Current Date and Time: ";
	time_t currentTime;
	struct tm * timeStructure;
	time(&currentTime);
	timeStructure = localtime(&currentTime);
	dateAndTime += asctime(timeStructure);

	return dateAndTime;
}



int main(int argc, char ** argv)
{
	using namespace monad;

	// 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 << version::official_name << endl;
	cout << "Testing " << name << "..." << endl << 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 runtests
	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";
	if (opts::optimize)
		makestr += " OPTIMIZE=on";
	if (!config["Make Options"].empty())
		makestr += " " + config["Make Options"][0];
#if DEBUG
	cout << makestr <<endl;
#endif

	// Compile with make
	system(makestr.c_str()); // yes, system is insecure if the user has control
	                         // over config.ini. But students don't.

	cout << endl << endl;
	int score = exec("./runtests");
	if (score < 0)
	{
		output::header("Running tests");
		cout << "Could not execute test cases" << endl << endl;
		score = 0;

		cout << endl;
		output::total_score(score);
	}

	return score;
}


void monad::processArgs(int argc, char ** argv)
{
    // Create OptionsMap for options and vector for positional arguments:
	OptionsMap options;
	vector<string> args;

	// Add our possible options to our map
	options.insert(make_pair("solution", false));
	options.insert(make_pair("newtests", true));
	options.insert(make_pair("clean",    opts::clean));
	options.insert(make_pair("update",   opts::update));
	options.insert(make_pair("staff",    false));
	options.insert(make_pair("optimize", opts::optimize));
	options.insert(make_pair("help", false));
	options.insert(make_pair("h",    false));
	options.insert(make_pair("info", false));

	// Read in options and arguments
	char * badopt = processOptions(argc, argv, options, args);
	if (badopt != NULL)
		exit(-1);
	
	// Save out options
	bool solution = options["solution"];
	bool newtests = options["newtests"];
	opts::clean   = options["clean"];
	opts::update  = options["update"];
	opts::staff   = options["staff"];

	// Help
	if (options["help"] || options["h"] ||
	    (!args.empty() && strcasecmp(args[0].c_str(), "help") == 0))
	{
		printHelp();
		exit(0);
	}

	// Info
	if (options["info"])
	{
		printInfo();
		exit(0);
	}

	// Check argument list length
	if (args.empty() || args.size() > 1)
	{
		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(args[0], '.');
	name = splitname[0];
	if (splitname.size() == 1)
		mp_part = no_mp_part;
	else
		mp_part = atoi(splitname[1].c_str());
	
	// Clean
	if (name == "clean")
	{
		system("/bin/rm -rf *_grade/ *_tests/ *_newtests/ *_solution/");
		exit(0);
	}

	gradeFolder = "./" + name + "_grade/";
	if (!exists(gradeFolder)) opts::clean = true;

	if (opts::clean)
		tempFolder = "";
	else
		tempFolder  = "./" + name + "_temp/";

	// Find source folder (i.e. ../mp1)
	if (solution)
		sourceFolder = updateFolder(name + "_solution/", false);
	else
		sourceFolder = getFolder(name + '/', false);

	// tests folder
	if (newtests)
		testsFolder = updateFolder(name + "_newtests/", false);
	else
		testsFolder = updateFolder(name + "_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::printHelp()
{
	cout << "Usage: monad ASSIGNMENT [solution]" << endl
	     << "Runs the tests for ASSIGNMENT (mp1, lab01, ...)" << endl
	     << "\'../ASSIGNMENT/\' must exist" << endl
	     << "If \'../ASSIGNMENT_tests/\' or any necessary library directories do not exist, these will be downloaded to ./ from SVN" << endl << endl
	     << "monad settings and individual test settings are stored in config.ini files" << endl
	     << "The following options are available for monad's ./config.ini:" << endl
	     << "[SVN Root]" << endl
	     << "; svn base url here (http://.../cs225/)" << endl
	     << "[Monad Files]" << endl
	     << "; Any files to be copied from monad ./ to ./ASSIGNMENT/ testing directory" << endl
	     << "; By default, runtests.cpp is used to run test cases:" << endl
	     << "runtests.cpp" << endl << endl
	     << "The following options are available for individual tests' ../ASSIGNMENT_tests/config.ini:" << endl
	     << "[Required Files]" << endl
	     << "; Any files that must be copied from the ../ASSIGNMENT directory" << endl
	     << "main.cpp" << endl
	     << "[Testing Files]" << endl
	     << "; Any files that must be copied from ../ASSIGNMENT_tests" << endl
	     << "; By default, unit_tests.cpp contains the test cases" << endl
	     << "unit_tests.cpp" << endl
	     << "[Libraries]" << endl
	     << "; Any external library folders to be present in the same directory as the" << endl
	     << "; testing directory. These should be mirrored in [SVN Root]/_public/" << endl
	     << "; The 'testutil' library in this directory is also available" << endl
	     << "EasyBMP" << endl
	     << endl;

}

void monad::printInfo()
{
	cout << version::official_name << endl
	     << "Version " << version::version_name << endl
		 << "Released " << version::date << endl
	     << "Developed by Jack Toole Spring/Fall 2011" << endl
	     << "Copyright 2011 Jack Toole" << endl
	     << "Full rights granted to Jack Toole. Rights to use and modify granted to" << endl
	     << "University of Illinois Urbana-Champaign Computer Science Data Structures" << endl
	     << "instructors and course staff" << endl;
}


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 & sourceFolder,
                 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 (sourceFolder    != "") assertExists(sourceFolder, 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) &&
		    (sourceFolder == "" || (permissions(preservedFile) & S_IWUSR) == 0))
		{
//!!			cout << "mv " << preservedFile << ' ' << destFile << endl;
			EXIT_IF_ERROR(rename(preservedFile.c_str(), destFile.c_str()));
		}
		else if (sourceFolder != "")
		{
			// copy the file from it's source
			string sourceFile = sourceFolder + files[i];
			assertExists(sourceFile, student_error_code);
//!!			cout << "cp " << sourceFile << ' ' << destFile << endl;
			EXIT_IF_ERROR(exec("/bin/cp", sourceFile.c_str(), destFile.c_str()) != 0);
		}
		else continue;

		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, "/usr/bin/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;

		int svnstatus = exec("/usr/bin/svn","co",svndir.c_str()); // No redirect, need user to type password
		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;
	}

	system(command.c_str());
}