diff --git a/ci_cd/.gitlab-ci.yml b/ci_cd/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..4ab793af61cc1090d9b621fc2d273265458ed651 --- /dev/null +++ b/ci_cd/.gitlab-ci.yml @@ -0,0 +1,11 @@ +stages: + - prebuild + - compile + - test + - extra + +include: + - local: 'ci_cd/problem3a.yml' + +default: + timeout: 5m diff --git a/ci_cd/problem3a.yml b/ci_cd/problem3a.yml new file mode 100644 index 0000000000000000000000000000000000000000..8dcb9171727950ecc6fff1d66e52b766d192608a --- /dev/null +++ b/ci_cd/problem3a.yml @@ -0,0 +1,47 @@ +prebuild_problem_3a: + stage: prebuild + script: + - | + # Check if source files exist + if [ ! -f "impl/Rubk.cpp" ]; then + echo "Rubk.cpp does not exist under impl directory"; + exit 1; + fi + - git clone https://agile.bu.edu/gitlab/configs/ec330/homeworks/homeworktwo.git hw2 + artifacts: + paths: + - hw2/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem3a"' + tags: [c++-17] + +compile_problem_3a: + stage: compile + needs: + - job: prebuild_problem_3a + artifacts: true + script: + - cp impl/Rubk.cpp hw2/tests/ + - cd hw2/tests + - make problem3a + artifacts: + paths: + - hw2/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem3a"' + tags: [c++-17] + +exec_problem_3a: + stage: test + needs: + - job: compile_problem_3a + artifacts: true + script: + - cd hw2/tests + - ./problem3a + artifacts: + paths: + - hw2/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem3a"' + tags: [c++-17] \ No newline at end of file diff --git a/ci_cd/problem3c.yml b/ci_cd/problem3c.yml new file mode 100644 index 0000000000000000000000000000000000000000..8bcc19cca22bca8bf79e357907e8f5d1f754a968 --- /dev/null +++ b/ci_cd/problem3c.yml @@ -0,0 +1,51 @@ +prebuild_problem_3c: + stage: prebuild + script: + - | + # Check if source files exist + if [ ! -f "main.cpp" ]; then + echo "main.cpp does not exist"; + exit 1; + fi + if [ ! -f "impl/Rubk.cpp" ]; then + echo "Rubk.cpp does not exist under impl directory"; + exit 1; + fi + - git clone https://agile.bu.edu/gitlab/configs/ec330/homeworks/homeworktwo.git hw2 + artifacts: + paths: + - hw2/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem3c"' + tags: [c++-17] + +compile_problem_3c: + stage: compile + needs: + - job: prebuild_problem_3c + artifacts: true + script: + - cp impl/Rubk.cpp main.cpp hw2/tests/ + - cd hw2/tests + - make problem3c + artifacts: + paths: + - hw2/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem3c"' + tags: [c++-17] + +exec_problem_3c: + stage: test + needs: + - job: compile_problem_3c + artifacts: true + script: + - cd hw2/tests + - ./problem3c + artifacts: + paths: + - hw2/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem3c"' + tags: [c++-17] \ No newline at end of file diff --git a/impl/Rubk.cpp b/impl/Rubk.cpp index 13dc5be6bccb46a21a8294adeef407313edacd60..83db843add77ab4f28cff04384b993b79f5122d1 100644 --- a/impl/Rubk.cpp +++ b/impl/Rubk.cpp @@ -3,12 +3,13 @@ // #include "../include/Rubk.h" +#include "../include/Enums.h" +#include +#include #include #include -#include "../include/Enums.h" - // HELPER FUNCTIONS /** * @param line The line to examine diff --git a/include/Enums.h b/include/Enums.h index 6d6c5cd5027acbdfff601420bfe6bbbd6c8b2a77..18a09e51d7cbf154460290a9c55de881222372b7 100644 --- a/include/Enums.h +++ b/include/Enums.h @@ -3,6 +3,7 @@ // #ifndef ENUM_H #define ENUM_H +#include #include /** diff --git a/tests/3c_testcases/test_0.txt b/tests/3c_testcases/test_0.txt new file mode 100644 index 0000000000000000000000000000000000000000..cbd859c5748e92d6c2c7736674885d596108c53b --- /dev/null +++ b/tests/3c_testcases/test_0.txt @@ -0,0 +1,11 @@ + R R B + Y Y O + Y Y G + +Y O O B B O Y B R Y G G +G G G Y O W O B R Y R W +R R W G G W O B W R B B + + O O B + W W R + W W G \ No newline at end of file diff --git a/tests/3c_testcases/test_1.txt b/tests/3c_testcases/test_1.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac33684419c27fbcbd0c7e12f4eb41650f0d4df7 --- /dev/null +++ b/tests/3c_testcases/test_1.txt @@ -0,0 +1,11 @@ + R Y O + R Y O + R Y O + +G G G Y O W B B B Y R W +G G G Y O W B B B Y R W +G G G Y O W B B B Y R W + + O W R + O W R + O W R diff --git a/tests/Enums.cpp b/tests/Enums.cpp new file mode 100644 index 0000000000000000000000000000000000000000..81c71710457c244e56f873dcdd57327bf19265c4 --- /dev/null +++ b/tests/Enums.cpp @@ -0,0 +1,35 @@ +// +// Created by Ari on 2/9/26. +// + +#include "Enums.h" + +#include +using namespace std; + +std::ostream& operator<<(ostream& os, const Color clr) { + return os << ColorToChar(clr); +} + +char ColorToChar(Color clr) { + return ColorToCharArray[static_cast(clr)]; +} + +Color CharToColor(const char ch) { + switch (std::toupper(static_cast(ch))) { + case 'W': + return Color::WHITE; + case 'Y': + return Color::YELLOW; + case 'R': + return Color::RED; + case 'O': + return Color::ORANGE; + case 'B': + return Color::BLUE; + case 'G': + return Color::GREEN; + default: + throw std::invalid_argument("invalid color character"); + } +} \ No newline at end of file diff --git a/tests/Enums.h b/tests/Enums.h new file mode 100644 index 0000000000000000000000000000000000000000..18a09e51d7cbf154460290a9c55de881222372b7 --- /dev/null +++ b/tests/Enums.h @@ -0,0 +1,76 @@ +// +// Created by Ari Trachtenberg on 2/6/26. +// +#ifndef ENUM_H +#define ENUM_H +#include +#include + +/** + * The available colors of a cell. + * {@code __BLANK__} indicates no color. + */ +enum class Color { GREEN, ORANGE, RED, WHITE, YELLOW, BLUE, BLANK_ }; + +/** + * Letters corresponding to the colors + */ +constexpr std::array(Color::BLANK_) + 1> + ColorToCharArray = {'G', 'O', 'R', 'W', 'Y', 'B', ' '}; + +/** + * @param clr The color to convert. + * @return The character corresponding to a {@link Color}. + */ +char ColorToChar(Color clr); + +/** + * @param ch The letter representing a {@link Color}. + * @return The {@link Color} corresponding to a given letter + */ +Color CharToColor(char ch); + +/** + * External method for displaying a color on the output + * stream {@code os}. + * @param os The output stream to which to produce a human-readable + * version of this cell. + * @param clr The color to display. + */ +std::ostream& operator<<(std::ostream& os, Color clr); + +/** + * The six faces of the cube. + */ +enum class FaceName { + FRONT, + LEFT, + RIGHT, + TOP, + BOTTOM, + AFT, + FACENAME_LAST // a sentinel for determining the number of faces +}; +constexpr size_t FACENAME_COUNT = static_cast(FaceName::FACENAME_LAST); + +/** + * The types of moves that can be made on the cube. + */ +enum CubeMoves { + LeftDown, + LeftUp, + RightDown, + RightUp, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + OverLeft, + OverRight, + OverFarLeft, + OverFarRight, + MOVE_LAST // a sentinel for determining the number of moves +}; +constexpr size_t CUBEMOVES_COUNT = MOVE_LAST; + +#endif // ENUM_H \ No newline at end of file diff --git a/tests/Face.h b/tests/Face.h new file mode 100644 index 0000000000000000000000000000000000000000..f166b576c34ff377e2cd8b5108d7c7734b2f778d --- /dev/null +++ b/tests/Face.h @@ -0,0 +1,103 @@ +// +// Created by Ari on 2/5/17. +// + +#ifndef RUBIKS_FACE_H +#define RUBIKS_FACE_H + +#include +#include + +#include "Enums.h" + +using namespace std; + +/** + * Represents one face of the {@link Rubk} cube, which is a + * square matrix of {@link Color}ed cells. + */ +class Face { + public: + // NESTED CLASSES + + /** + * Represents a row; no bounds checking. + */ + class Row { + public: + explicit constexpr Row(const int theRow) : value(theRow) {} + const int value; + }; + + /** + * Represents a column; no bounds checking. + */ + class Column { + public: + explicit constexpr Column(const int theCol) : value(theCol) {} + const int value; + }; + + // CONSTRUCTORS + /** + * Constructs one [len] x [len] face of a {@link Rubk} cube, + * all of whose cells have color {@code theColor}. + * @param len The linear dimension of the face. + * @requires len >= 3. + */ + Face(const Color theColor, const int len = 3) { + cells = std::vector(len, std::vector(len, theColor)); + } + + // GETTERS / SETTERS + /** + * @param row the row in which the cell can be found + * @param col the column in which the cell can be found + * @return The color of the cell at column {@code col} and + * row {@code row} of the face. + * @requires 0 <= row,col <= {@link faceLen()} - 1 + */ + [[nodiscard]] Color get(const Row row, const Column col) const { + return cells[col.value][row.value]; + } + + /** + * Change the color of a cell at the given row and column on the face. + * + * @param row the row in which the cell can be found + * @param col the column in which the cell can be found + * @param newColor The new color of the cell. + * @requires 0 <= row,col <= {@link faceLen()} - 1 + */ + void set(const Row row, const Column col, const Color newColor) { + cells[col.value][row.value] = newColor; + } + + // INFORMATIONAL + /** + * @param theRow the row of the face to return + * @return a human-readable string representing row {@code theRow} of the face + * @requires 0 <= row,col <= {@link faceLen()} - 1 + */ + [[nodiscard]] string printRow(const Row theRow) const { + stringstream result; + for (int theCol = 0; theCol < faceLen(); theCol++) + result << get(theRow, Column(theCol)) << " "; + return result.str(); + } + + /** + * @return The linear length of a side of the face. + */ + [[nodiscard]] int faceLen() const { return static_cast(cells.size()); } + + private: + /** + * Colored cells that constitute this face. + * This is represented as a vector of rows of the face. + * Each row, in turn, is a vector of {@link Color}s. + */ + vector > cells; +}; + +#endif // RUBIKS_FACE_H diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..7b18d90f5d4107f00b819e372886f5b287eca237 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,44 @@ +# Makefile, generated with support of ChatGPT + +# Compiler and flags +CXX = g++ -O2 +CXXFLAGS = -Wall -std=c++17 + +# Source files +PROBLEM3A_SRCS = testMoves.cpp Rubk.cpp Enums.cpp +SOLVER_SRC = main.cpp Rubk.cpp Enums.cpp +PROBLEM3C_SRCS = testUnscramble.cpp Rubk.cpp Enums.cpp +SRCS = $(PROBLEM3A_SRCS) $(SOLVER_SRC) $(PROBLEM3C_SRCS) + +# Object files +PROBLEM3A_OBJS = testMoves.o Rubk.o Enums.o +SOLVER_OBJS = main.o Rubk.o Enums.o +PROBLEM3C_OBJS = testUnscramble.o Rubk.o Enums.o +OBJS = $(PROBLEM3A_OBJS) $(SOLVER_OBJS) $(PROBLEM3C_OBJS) + +# Executable names +PROBLEM3A_EXEC = problem3a +SOLVER_EXEC = solver +PROBLEM3C_EXEC = problem3c + +# Default target to build the executable +all: $(PROBLEM3A_EXEC) $(SOLVER_EXEC) $(PROBLEM3C_EXEC) + +# Rule to build the executable from object files +$(PROBLEM3A_EXEC): $(PROBLEM3A_OBJS) Makefile + $(CXX) $(CXXFLAGS) -o $(PROBLEM3A_EXEC) $(PROBLEM3A_OBJS) + +$(SOLVER_EXEC): $(SOLVER_OBJS) Makefile + $(CXX) $(CXXFLAGS) -o $(SOLVER_EXEC) $(SOLVER_OBJS) + +$(PROBLEM3C_EXEC): $(PROBLEM3C_OBJS) Makefile + $(CXX) $(CXXFLAGS) -o $(PROBLEM3C_EXEC) $(PROBLEM3C_OBJS) + +# Rules to build object files from source files, with dependency on the Common.h header +%.o: %.cpp Makefile + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# Clean target to remove compiled files +clean: + rm -f $(OBJS) $(EXEC) + diff --git a/tests/Rubk.h b/tests/Rubk.h new file mode 100644 index 0000000000000000000000000000000000000000..00484916756768b53afcc82ce6acf1729660f3d6 --- /dev/null +++ b/tests/Rubk.h @@ -0,0 +1,126 @@ +// +// Created by Ari on 2/4/17. +// + +#ifndef RUBIKS_CUBE_H +#define RUBIKS_CUBE_H + +#include +#include + +#include "Enums.h" +#include "Face.h" + +using namespace std; + +/** + * Represents a configuration of a Rubk's cube. The cube represents an + * immutable three-dimensional {@code len x len x len} structure, where + * {@code len} is specified in the constructor. + */ +class Rubk { + public: + // CONSTRUCTORS + + /** + * Constructs a [len] x [len] x [len] cube in standard coloring order. + * @param len The linear length of any cube size. + */ + Rubk(int len); + + /** + * Copy constructor from an existing Rubk. + * @param other The Rubk to copy. + */ + Rubk(Rubk const &other); + + /** + * Constructs a Rubk from its flat profile. + * @throws invalid_argument if the profile string is in the wrong format. + * @param flatProfile a string of the following form: + * B R R + * B R R + * B R R + * + * B B B R W W R R R B B W + * B B B R W W R R R B B W + * B B B R W W R R R B B W + * + * W W W + * W W W + * W W W + */ + Rubk(const string &flatProfile); + + // MANIPULATORS + + /** + * Makes a move and returns the resulting cube + * @param theMove the move to make + * @return a new cube representing the given move from the current cube. + */ + [[nodiscard]] Rubk makeMove(CubeMoves theMove) const; + + /** + * @param direction if true, rotates clockwise; else rotates counter-clockwise + * @param theFaceName The face to rotate. + * @return Rotates Face {@code theFaceName} 90 degrees and returns the resulting cube. + */ + [[nodiscard]] Rubk rotate(bool direction, FaceName theFaceName) const; + + // INFORMATION + + /** + * + * @return true iff each face is monochromal + * (i.e., all cells on that face are the same color) + */ + [[nodiscard]] bool isUnscrambled() const; + + /** + * @return The linear length of the cube. + */ + [[nodiscard]] int getLen() const { return cubeLen; } + + Face &getFace(const FaceName theFace) { return _faces.at(theFace); } + [[nodiscard]] const Face &getFace(const FaceName theFace) const { + return _faces.at(theFace); + } + + /** + * @param theFaceName The cube face where the cell resides. + * @param theRow The row of the cell on the given face. + * @param theColumn The column of the cell on the given face. + * @return The color of the specified cell of the cube. + */ + [[nodiscard]] Color getCellColor(FaceName theFaceName, Face::Row theRow, + Face::Column theColumn) const; + + /** + * @param theFaceName The cube face where the cell resides. + * @param theRow The row of the cell on the given face. + * @param theColumn The column of the cell on the given face. + * @param newColor The new color of the cell. + * @return Sets color of the specified cell of the cube to {@code newColor}. + */ + void setCellColor(FaceName theFaceName, Face::Row theRow, + Face::Column theColumn, Color newColor); + + /** + * @return A flattened representation of this cube. + */ + [[nodiscard]] string printFlattened() const; + + private: + // FIELDS + int cubeLen; // linear dimension of one side + + /** the faces of the cube */ + map _faces; +}; + +// public methods +// ... stream output +std::ostream &operator<<(std::ostream &os, const Rubk &cube); + +#endif // RUBIKS_CUBE_H diff --git a/tests/testMoves.cpp b/tests/testMoves.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0b1dc243bca86f9f49484c01922ed8fa8d9d9e03 --- /dev/null +++ b/tests/testMoves.cpp @@ -0,0 +1,138 @@ +// +// Created by Kevin on 2/10/2026. +// +#include +#include +#include + +#include "Rubk.h" // make sure include path matches your repo structure +#include "Enums.h" + +using namespace std; + +// helper function: consistent failure printing +bool failExample(const char* testName, + const std::string& msg, + const std::string& expected, + const std::string& got) { + std::cerr << "[" << testName << " FAILED] " << msg + << ": expected " << expected << ", got " << got << "\n"; + return false; +} + +// helper: compare two cubes by flattened string +static bool sameCube(const Rubk& a, const Rubk& b) { + return a.printFlattened() == b.printFlattened(); +} + +// helper: apply a move k times +static Rubk applyK(const Rubk& c, CubeMoves m, int k) { + Rubk out(c); + for (int i = 0; i < k; i++) out = out.makeMove(m); + return out; +} + +// helper: map move -> its inverse +static CubeMoves inverseMove(CubeMoves m) { + switch (m) { + case LeftDown: return LeftUp; + case LeftUp: return LeftDown; + case RightDown: return RightUp; + case RightUp: return RightDown; + case TopLeft: return TopRight; + case TopRight: return TopLeft; + case BottomLeft: return BottomRight; + case BottomRight: return BottomLeft; + default: + // We don't test Over* in problem 3a + throw std::out_of_range("inverseMove: unsupported move in this test"); + } +} + +// -------------------- Basic identity: move then inverse = identity -------------------- +bool test_0() { + const char* T = "Move then inverse should return original cube"; + Rubk start(3); + + const std::array moves = { + LeftDown, LeftUp, RightDown, RightUp, + TopLeft, TopRight, BottomLeft, BottomRight + }; + + for (CubeMoves m : moves) { + Rubk got = start.makeMove(m).makeMove(inverseMove(m)); + if (!sameCube(start, got)) { + return failExample( + T, + "start.makeMove(m).makeMove(inverse(m)) should equal start", + "same flattened cube", + "different flattened cube" + ); + } + } + + return true; +} + +// -------------------- Inverse direction sanity: inverse then move = identity -------------------- +bool test_1() { + const char* T = "Inverse then move should return original cube"; + Rubk start(3); + + const std::array moves = { + LeftDown, LeftUp, RightDown, RightUp, + TopLeft, TopRight, BottomLeft, BottomRight + }; + + for (CubeMoves m : moves) { + Rubk got = start.makeMove(inverseMove(m)).makeMove(m); + if (!sameCube(start, got)) { + return failExample( + T, + "apply move and it's reverse move should equal start", + "same flattened cube", + "different flattened cube" + ); + } + } + + return true; +} + +// -------------------- Group property: applying same quarter-turn 4 times returns identity -------------------- +bool test_2() { + const char* T = "Applying a quarter-turn 4 times returns original cube"; + Rubk start(3); + + const std::array quarterTurns = { + LeftUp, RightUp, TopRight, BottomRight + }; + + for (CubeMoves m : quarterTurns) { + Rubk got = applyK(start, m, 4); + if (!sameCube(start, got)) { + return failExample( + T, + "apply same move 4 times should equal start", + "same flattened cube", + "different flattened cube" + ); + } + } + + return true; +} + +int main() { + bool results[] = { test_0(), test_1(), test_2()}; + + bool allPassed = true; + for (size_t ii = 0; ii < std::size(results); ii++) { + cout << "Test of problem " << to_string(ii) << ": " + << (results[ii] ? "passed" : "failed") << endl; + allPassed &= results[ii]; + } + + if (allPassed) exit(0); + else exit(-1); +} diff --git a/tests/testUnscramble.cpp b/tests/testUnscramble.cpp new file mode 100644 index 0000000000000000000000000000000000000000..55b498536dfbdbdbe1e8c391f387986c5e963df8 --- /dev/null +++ b/tests/testUnscramble.cpp @@ -0,0 +1,236 @@ +// +// Created by Kevin on 2/11/2026. +// +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() + #include + namespace fs = std::filesystem; +#else + #error "C++17 required for this test runner." +#endif + +#include "Rubk.h" +#include "Enums.h" + +using namespace std; + +// -------------------- CONFIG -------------------- +static const char* SOLVER_CMD = "./solver"; + +// Directory containing test_*.txt +static const char* TESTCASE_DIR = "./3c_testcases"; + +// -------------------- helper: fail printing -------------------- +bool failExample(const char* testName, + const std::string& msg, + const std::string& expected, + const std::string& got) { + std::cerr << "[" << testName << " FAILED] " << msg + << ": expected " << expected << ", got " << got << "\n"; + return false; +} + +// -------------------- helper: read whole file -------------------- +static std::string readFileToString(const fs::path& p) { + std::ifstream in(p, std::ios::binary); + if (!in) throw std::runtime_error("Cannot open testcase file: " + p.string()); + std::ostringstream ss; + ss << in.rdbuf(); + return ss.str(); +} + +// -------------------- helper: run solver and capture stdout -------------------- +static std::string runSolverCaptureStdout(const std::string& flatProfile) { + // Temp file avoids stdin quoting problems; then redirect stdin from it. + const char* tmpIn = "rubk_tmp_input.txt"; + { + FILE* f = std::fopen(tmpIn, "wb"); + if (!f) throw std::runtime_error("Cannot create temp input file."); + std::fwrite(flatProfile.data(), 1, flatProfile.size(), f); + std::fclose(f); + } + + std::string cmd = std::string(SOLVER_CMD) + " < " + tmpIn; + FILE* pipe = popen(cmd.c_str(), "r"); + if (!pipe) { + std::remove(tmpIn); + throw std::runtime_error("Failed to run solver via popen/_popen."); + } + + std::string out; + char buffer[4096]; + while (std::fgets(buffer, sizeof(buffer), pipe)) { + out += buffer; + } + + pclose(pipe); + std::remove(tmpIn); + return out; +} + +// -------------------- helper: check samples on selected faces -------------------- +static bool faceSamplesMatch(const Rubk& cube, FaceName face) { + const int n = cube.getLen(); + const Color ref = cube.getCellColor(face, Face::Row(0), Face::Column(0)); + + const Color center = cube.getCellColor(face, Face::Row(n/2), Face::Column(n/2)); + const Color bottomRight = cube.getCellColor(face, Face::Row(n-1), Face::Column(n-1)); + + return (center == ref) && (bottomRight == ref); +} + +static bool isLikelySolved(const Rubk& cube) { + // choose FRONT, TOP, LEFT + return faceSamplesMatch(cube, FaceName::FRONT) && + faceSamplesMatch(cube, FaceName::TOP) && + faceSamplesMatch(cube, FaceName::LEFT); +} + +// -------------------- helper: parse moves strictly -------------------- +static bool parseMoveLine(const std::string& line, CubeMoves& outMove) { + if (line == "LeftDown") { outMove = LeftDown; return true; } + if (line == "LeftUp") { outMove = LeftUp; return true; } + if (line == "RightDown") { outMove = RightDown; return true; } + if (line == "RightUp") { outMove = RightUp; return true; } + if (line == "TopLeft") { outMove = TopLeft; return true; } + if (line == "TopRight") { outMove = TopRight; return true; } + if (line == "BottomLeft") { outMove = BottomLeft; return true; } + if (line == "BottomRight") { outMove = BottomRight; return true; } + return false; +} + +static std::vector parseMovesStrict(const std::string& stdoutText) { + std::vector moves; + std::istringstream iss(stdoutText); + std::string line; + + while (std::getline(iss, line)) { + // Trim trailing CR for Windows line endings + if (!line.empty() && line.back() == '\r') line.pop_back(); + + // Allow blank lines (including trailing blank lines) + if (line.empty()) continue; + + CubeMoves m; + if (!parseMoveLine(line, m)) { + throw std::runtime_error("Invalid output line: '" + line + "'"); + } + moves.push_back(m); + } + return moves; +} + +// -------------------- helper: apply moves -------------------- +static Rubk applyMoves(const Rubk& start, const std::vector& moves) { + Rubk cur(start); + for (CubeMoves m : moves) cur = cur.makeMove(m); + return cur; +} + +// -------------------- single testcase runner -------------------- +static bool runOneTestcase(const fs::path& testcasePath) { + const std::string name = testcasePath.filename().string(); + const std::string flat = readFileToString(testcasePath); + + Rubk start(flat); + + std::string out; + try { + out = runSolverCaptureStdout(flat); + } catch (const std::exception& e) { + return failExample(name.c_str(), "failed to run solver", "solver runs", e.what()); + } + + std::vector moves; + try { + moves = parseMovesStrict(out); + } catch (const std::exception& e) { + return failExample(name.c_str(), + "stdout format invalid (must be only move names, one per line)", + "valid move names only", + e.what()); + } + + Rubk end = applyMoves(start, moves); + + if (!isLikelySolved(end)) { + // give a small hint: show first few output chars (avoid huge logs) + std::string snippet = out.substr(0, std::min(200, out.size())); + return failExample(name.c_str(), + "applying solver moves did not unscramble cube", + "unscrambled cube", + "still scrambled; solver output snippet: " + snippet); + } + + return true; +} + +// -------------------- collect testcases -------------------- +static std::vector collectTestcases() { + std::vector files; + fs::path dir(TESTCASE_DIR); + + if (!fs::exists(dir) || !fs::is_directory(dir)) { + throw std::runtime_error(std::string("Testcase dir not found: ") + TESTCASE_DIR); + } + + for (const auto& entry : fs::directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + const auto p = entry.path(); + const auto fname = p.filename().string(); + + // match test_*.txt + if (fname.rfind("test_", 0) == 0 && + p.extension() == ".txt") { + files.push_back(p); + } + } + + std::sort(files.begin(), files.end()); + return files; +} + +int main() { + + bool allPassed = true; + size_t idx = 0; + + // ---------- collect testcases ---------- + std::vector tests; + try { + tests = collectTestcases(); + } catch (const std::exception& e) { + std::cerr << "[FAILED] " << e.what() << "\n"; + return -1; + } + + if (tests.empty()) { + std::cerr << "[FAILED] No test_*.txt found in " << TESTCASE_DIR << "\n"; + return -1; + } + + // ---------- run solver tests ---------- + for (const auto& tc : tests) { + bool ok = runOneTestcase(tc); + + cout << "Testcase " << idx++ + << " (" << tc.filename().string() << "): " + << (ok ? "passed" : "failed") << "\n"; + + allPassed &= ok; + } + + return allPassed ? 0 : -1; +} +