-
Benedikt Conze authoredBenedikt Conze authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
integration_test.cpp 17.88 KiB
#include "data/csv_dump.hpp"
#include "data/data_manager.hpp"
#include "data/graph.hpp"
#include "data/nodes/bonus_points.hpp"
#include "data/nodes/dynexite_exam.hpp"
#include "data/nodes/student_data.hpp"
#include "data/nodes/user_grade_config.hpp"
#include "data/nodes/user_point_config.hpp"
#include "file_io/helper.hpp"
#include "util/heuristics.hpp"
#include "util/strings.hpp"
#include "view/main_window/export_rwthonline.hpp"
#include <gtest/gtest.h>
#include <QRegularExpression>
#include <QSharedPointer>
#include <QString>
#include <algorithm>
#include <array>
#include <filesystem>
#include <regex>
// TODO
const auto fineOpenDynexite = "mostlyopennew";
const auto fineClosedDynexite = "mostlyclosednew";
constexpr auto nonregisteredOffset = 53;
enum FirstAttempts {
NoFirstAttempts,
LessThan50PercPoints,
Between50And60,
MoreThan60PercPoints,
AllFirstAttempts
};
[[nodiscard]] QString getRwthOnlineFileName(const FirstAttempts firstAttempt) {
switch (firstAttempt) {
case NoFirstAttempts:
return "nofirstnew";
case LessThan50PercPoints:
return "less50new";
case Between50And60:
return "50to60new";
case MoreThan60PercPoints:
return "more60new";
case AllFirstAttempts:
return "onlyfirstnew";
}
assert(false); // todo
return "nofirstnew";
}
[[nodiscard]] Grade readGrade(QString s, bool *ok) {
// Grades may be printed with a comma instead of a point for backwards
// compatibility
s.replace(',', '.');
*ok = true;
if (s == "1.0") {
return Grade::grade1_0;
} else if (s == "1.3") {
return Grade::grade1_3;
} else if (s == "1.7") {
return Grade::grade1_7;
} else if (s == "2.0") {
return Grade::grade2_0;
} else if (s == "2.3") {
return Grade::grade2_3;
} else if (s == "2.7") {
return Grade::grade2_7;
} else if (s == "3.0") {
return Grade::grade3_0;
} else if (s == "3.3") {
return Grade::grade3_3;
} else if (s == "3.7") {
return Grade::grade3_7;
} else if (s == "4.0") {
return Grade::grade4_0;
} else if (s == "5.0") {
return Grade::grade5_0;
} else {
ADD_FAILURE() << "Unknown grade: " << s.toStdString();
*ok = false;
return Grade::NR_VALS;
}
}
void testExportedFile(QTextStream &ts, const QString &pattern,
const bool useBonus,
const FirstAttempts firstAttemptConfig,
Points &avgFirstAttemptClosedPoints,
const GradingType fineGradingType,
const TaskEliminationType taskEliminationType,
const bool checkNonAttended,
const std::function<int(int)> &matriculationIncrementor) {
QString line;
const auto coarseGradingType = fineGradingType == GradingType::Closed
? GradingType::Open
: GradingType::Closed;
// Skip header
ts.readLineInto(&line);
// Attended students
{
using enum GradingType;
SCOPED_TRACE("Full regex: " + pattern.toStdString());
// Read export line by line
auto matriculationCounter = matriculationIncrementor(0);
// todo: maybe do differently
const auto maxMatriculation = 53602;
QRegularExpression re(pattern);
while (matriculationCounter < maxMatriculation) {
ts.readLineInto(&line);
const auto match = re.match(line);
SCOPED_TRACE("Line: " + line.toStdString());
EXPECT_TRUE(match.hasMatch());
QList<bool> oks;
oks.reserve(8);
// captures start at 1 (0 is the whole match)
int captureCounter = 1;
// Matriculation number
const auto matriculationNr =
match.captured(captureCounter++).toInt(&oks.emplace_back());
// Combined grade
EnumStorage<Grade> grade;
grade[Combined] = readGrade(match.captured(captureCounter++),
&oks.emplace_back());
EnumStorage<Points, GradingType, 2> earnedPoints, maxPoints,
bonusPoints;
auto parsedAvgFirstAttemptClosedPoints = Points{0.};
for (const auto t : {Closed, Open}) {
// Remark
// If we use bonus points, the position of found groups
// differs depending on if this exam part was passed or not
// (because the regex has a disjunction with capture groups
// in the different branches)
bool firstBranch = true;
if (useBonus && match.captured(captureCounter).isEmpty()) {
captureCounter += 4;
firstBranch = false;
}
earnedPoints[t] = Points{match.captured(captureCounter++)
.toDouble(&oks.emplace_back())};
maxPoints[t] = Points{match.captured(captureCounter++)
.toDouble(&oks.emplace_back())};
if (useBonus) {
if (firstBranch) {
bonusPoints[t] =
Points{match.captured(captureCounter++)
.toDouble(&oks.emplace_back())};
grade[t] = readGrade(match.captured(captureCounter++),
&oks.emplace_back());
// Skip second branch
captureCounter += 4;
} else {
grade[t] = readGrade(match.captured(captureCounter++),
&oks.emplace_back());
bonusPoints[t] =
Points{match.captured(captureCounter++)
.toDouble(&oks.emplace_back())};
}
} else {
grade[t] = readGrade(match.captured(captureCounter++),
&oks.emplace_back());
}
// average
if (t == Closed && firstAttemptConfig != NoFirstAttempts) {
parsedAvgFirstAttemptClosedPoints =
Points{match.captured(captureCounter++)
.toDouble(&oks.emplace_back())};
}
}
EXPECT_TRUE(std::ranges::all_of(oks, [](bool b) { return b; }))
<< "Some conversions failed.";
EXPECT_EQ(matriculationNr, matriculationCounter);
EnumStorage<Points, GradingType, 2> targetPoints;
targetPoints[fineGradingType] = Points{
(matriculationNr - 1) / 51 * 0.02 +
(taskEliminationType == TaskEliminationType::creditFull ? 3.
: 0.)};
targetPoints[coarseGradingType] = Points{
(matriculationNr - 1) % 51 * 0.2 +
(taskEliminationType == TaskEliminationType::creditFull ? 3.
: 0.)};
EXPECT_DOUBLE_EQ(earnedPoints[fineGradingType].p,
targetPoints[fineGradingType].p);
EXPECT_DOUBLE_EQ(earnedPoints[coarseGradingType].p,
targetPoints[coarseGradingType].p);
EXPECT_EQ(maxPoints[fineGradingType],
Points{21. + (taskEliminationType ==
TaskEliminationType::creditFull
? 3.
: 0.)});
EXPECT_EQ(maxPoints[coarseGradingType],
Points{10. + (taskEliminationType ==
TaskEliminationType::creditFull
? 3.
: 0.)});
if (avgFirstAttemptClosedPoints < Points{0.}) {
avgFirstAttemptClosedPoints = parsedAvgFirstAttemptClosedPoints;
}
EXPECT_DOUBLE_EQ(avgFirstAttemptClosedPoints.p,
parsedAvgFirstAttemptClosedPoints.p);
matriculationCounter =
matriculationIncrementor(matriculationCounter);
}
}
if (checkNonAttended) {
const auto notAttendedRegexString = "^\\d+;\\d;X;;;$";
SCOPED_TRACE(std::string("Not attended regex: ") +
notAttendedRegexString);
QRegularExpression reNotAttended(notAttendedRegexString);
while (ts.readLineInto(&line)) {
const auto match = reNotAttended.match(line);
SCOPED_TRACE("Line: " + line.toStdString());
ASSERT_TRUE(match.hasMatch());
}
}
// TODO: also check exportManualAllStudents?
}
/*
* Parameter meaning:
* Parameter 1 (string): Name of the dynexite file
* Parameter 2 (FirstAttempts): Number and points of first attempt students
* Parameter 3 (GradeBoundaryStepSize): Step size of grade boundaries
* Parameter 4 (bool): Whether or not the exam has a bonus file
* Parameter 5 (TaskEliminationType): Elimination type of task 3
*/
class IntegrationFixture
: public testing::TestWithParam<
std::tuple<QString, FirstAttempts, GradeBoundaryStepSize, bool,
TaskEliminationType>> {};
INSTANTIATE_TEST_SUITE_P(
IntegrationFixtureInstantiation, IntegrationFixture,
testing::Combine(testing::Values(fineOpenDynexite, fineClosedDynexite),
testing::Values(NoFirstAttempts, LessThan50PercPoints,
Between50And60, MoreThan60PercPoints,
AllFirstAttempts),
testing::Values(GradeBoundaryStepSize::Min1_00,
GradeBoundaryStepSize::Min0_50,
GradeBoundaryStepSize::Min0_25),
testing::Bool(),
testing::Values(TaskEliminationType::withdrawTask,
TaskEliminationType::creditFull)));
TEST_P(IntegrationFixture, myNameIntegrationTest) {
const auto dynexiteFileName = std::get<0>(GetParam());
const auto firstAttemptConfig = std::get<1>(GetParam());
const auto gradeBoundaryStepSize = std::get<2>(GetParam());
const auto useBonus = std::get<3>(GetParam());
const auto taskEliminationType = std::get<4>(GetParam());
const auto fineGradingType = dynexiteFileName == "mostlyopennew"
? GradingType::Open
: GradingType::Closed;
SCOPED_TRACE(
"Reading in file " + dynexiteFileName.toStdString() +
", first attempt setting: " + std::to_string(firstAttemptConfig) +
", bonus points: " + std::to_string(useBonus));
// Open the exam with the given files
std::string file{__FILE__};
std::filesystem::path rootPath{};
if (file.find('/') != std::string::npos) {
rootPath = std::filesystem::path({file.substr(0, file.rfind('/'))});
} else {
rootPath = std::filesystem::path({file.substr(0, file.rfind('\\'))});
}
std::cout << "Root path: " << rootPath.string() << std::endl;
const auto dynexitePath =
rootPath / "dynexite" / (dynexiteFileName.toStdString() + ".csv");
const auto rwthOnlinePath =
rootPath / "rwthonline" /
(getRwthOnlineFileName(firstAttemptConfig).toStdString() + ".csv");
const auto bonusPath = rootPath / "bonus" / "bonus.csv";
ASSERT_TRUE(exists(dynexitePath))
<< "Found no file at " << dynexitePath.string()
<< ", root path: " << rootPath.string()
<< ", current path: " << std::filesystem::current_path().string();
ASSERT_TRUE(exists(rwthOnlinePath))
<< "Found no file at " << dynexitePath.string()
<< ", root path: " << rootPath.string()
<< ", current path: " << std::filesystem::current_path().string();
ASSERT_TRUE(!useBonus || exists(bonusPath))
<< "Found no file at " << dynexitePath.string()
<< ", root path: " << rootPath.string()
<< ", current path: " << std::filesystem::current_path().string();
QSharedPointer<CsvDump> dynexiteDump;
QSharedPointer<CsvDump> rwthDump;
QSharedPointer<CsvDump> bonusDump;
ASSERT_NO_FATAL_FAILURE(dynexiteDump = getDump(dynexitePath));
ASSERT_NO_FATAL_FAILURE(rwthDump = getDump(rwthOnlinePath));
if (useBonus) {
ASSERT_NO_FATAL_FAILURE(bonusDump = getDump(bonusPath));
} else {
// Empty bonus point file
bonusDump = QSharedPointer<CsvDump>::create();
}
DynexiteExam dynexite(*dynexiteDump);
StudentData students(*rwthDump);
BonusPoints bonus(*bonusDump);
UserGradeConfig gc;
UserPointConfig pc;
pc.mGradeBoundaryStepSize = gradeBoundaryStepSize;
const auto success =
DataManager::loadNewExam(dynexite, students, bonus, gc, pc);
ASSERT_TRUE(success);
DataManager::makeNewCurrent();
// Eliminate task with random points
DataManager::taskEliminationChange(3, taskEliminationType);
// TODO: change grade boundaries
// REGEX BUILDING
// The original strings contain characters which need to be escaped
const auto escapedRemarkNoBonus =
QString(str::fileExport::rwthOnlineRemarkNoBonus)
.replace('(', "\\(")
.replace(')', "\\)")
.replace('.', "\\.");
const auto escapedRemarkPassedBonus =
QString(str::fileExport::rwthOnlineRemarkPassedBonus)
.replace('(', "\\(")
.replace(')', "\\)")
.replace('.', "\\.")
.replace('+', "\\+");
const auto escapedRemarkNotPassedBonus =
QString(str::fileExport::rwthOnlineRemarkNotPassedBonus)
.replace('(', "\\(")
.replace(')', "\\)")
.replace('.', "\\.")
.replace('+', "\\+");
// Build up regex pattern for export lines (most importantly the remark)
const auto decimalNumberPattern = QString("(\\d+\\.?\\d*)");
// Grade inside the remark (written with a point)
const auto gradePattern = QString("(\\d\\.\\d)");
/*const auto linePattern =
QString("^;;;;(\\d+);;;;;\\d;(\\d,\\d);%1 - %2;;;;;;;;;;;;;$");*/
const auto linePattern = QString("^(\\d+);\\d;(\\d,\\d);%1 - %2;;$");
const auto nonregisteredLinePattern =
QString("^(\\d+);(\\d,\\d);%1 - %2$"); // todo
const auto noBonusRemarkPattern =
QString(escapedRemarkNoBonus)
.arg("%20", decimalNumberPattern, gradePattern,
decimalNumberPattern);
const auto passedBonusRemarkPattern =
QString(escapedRemarkPassedBonus)
.arg("%20", decimalNumberPattern, gradePattern,
decimalNumberPattern, decimalNumberPattern);
const auto notPassedBonusRemarkPattern =
QString(escapedRemarkNotPassedBonus)
.arg("%20", decimalNumberPattern, gradePattern,
decimalNumberPattern, decimalNumberPattern);
const auto firstAnyPattern =
QString(str::fileExport::rwthOnlineRemarkAverageFirstAny)
.arg(decimalNumberPattern);
const auto firstNonePattern =
QString(str::fileExport::rwthOnlineRemarkAverageFirstNone)
.replace('.', "\\.");
QString closedPattern, openPattern;
if (useBonus) {
if (firstAttemptConfig == NoFirstAttempts) {
closedPattern =
QString("(?:%1|%2)")
.arg(passedBonusRemarkPattern.arg("Closed"),
notPassedBonusRemarkPattern.arg("Closed")) +
firstNonePattern;
} else {
closedPattern =
QString("(?:%1|%2)")
.arg(passedBonusRemarkPattern.arg("Closed"),
notPassedBonusRemarkPattern.arg("Closed")) +
firstAnyPattern;
}
openPattern = QString("(?:%1|%2)")
.arg(passedBonusRemarkPattern.arg("Open"),
notPassedBonusRemarkPattern.arg("Open"));
} else {
if (firstAttemptConfig == NoFirstAttempts) {
closedPattern =
QString("%1").arg(noBonusRemarkPattern.arg("Closed")) +
firstNonePattern;
} else {
closedPattern =
QString("%1").arg(noBonusRemarkPattern.arg("Closed")) +
firstAnyPattern;
}
openPattern = QString("%1").arg(noBonusRemarkPattern.arg("Open"));
}
const auto fullPattern = linePattern.arg(closedPattern, openPattern);
const auto fullNonregisteredPattern =
nonregisteredLinePattern.arg(closedPattern, openPattern);
Points avgFirstAttemptClosedPoints = Points{-1.};
// ******************************* KNOWN STUDENTS //
{
QString out = "";
QTextStream ts{&out};
ASSERT_TRUE(exportKnownStudents(nullptr, &ts));
// ******************************* //
ASSERT_NO_FATAL_FAILURE(testExportedFile(
ts, fullPattern, useBonus, firstAttemptConfig,
avgFirstAttemptClosedPoints, fineGradingType, taskEliminationType,
true, [](const int i) {
return (i + 1) % nonregisteredOffset == 0 ? i + 2 : i + 1;
}));
}
// ******************************* UNKNOWN STUDENTS //
{
QString out = "";
QTextStream ts{&out};
ASSERT_TRUE(exportUnknownStudents(nullptr, &ts));
// ******************************* //
ASSERT_NO_FATAL_FAILURE(testExportedFile(
ts, fullNonregisteredPattern, useBonus, firstAttemptConfig,
avgFirstAttemptClosedPoints, fineGradingType, taskEliminationType,
false, [](const int i) { return i + nonregisteredOffset; }));
}
// TODO: check that avg is always the same and less than 50/between 50 and
// 60/more than 60
}