From abdcaca8dded005c2594decdc6d7c20e56a69578 Mon Sep 17 00:00:00 2001 From: Evan Hughes Date: Thu, 23 Oct 2025 19:56:24 +1000 Subject: [PATCH] feat: added light conformance for staff to help troubleshoot --- src/main/java/chalkbox/commands/Grade.java | 1 + src/main/java/chalkbox/config/Config.java | 5 + .../stages/conformance/ConformanceLite.java | 147 ++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 src/main/java/chalkbox/stages/conformance/ConformanceLite.java diff --git a/src/main/java/chalkbox/commands/Grade.java b/src/main/java/chalkbox/commands/Grade.java index 2388d9f..637d522 100644 --- a/src/main/java/chalkbox/commands/Grade.java +++ b/src/main/java/chalkbox/commands/Grade.java @@ -83,6 +83,7 @@ private Stage getStage(String name, Config config) { case "ai" -> config.toAI(); case "codestyle" -> config.toCodestyle(); case "conformance" -> config.toConformance(); + case "conformance-lite" -> config.toConformanceLight(); case "functionality" -> config.toFunctionality(); case "mutation" -> config.toMutation(); case "tlc" -> config.toTLC(); diff --git a/src/main/java/chalkbox/config/Config.java b/src/main/java/chalkbox/config/Config.java index 23f5b5e..5038936 100644 --- a/src/main/java/chalkbox/config/Config.java +++ b/src/main/java/chalkbox/config/Config.java @@ -6,6 +6,7 @@ import chalkbox.stages.functionality.Functionality; import chalkbox.stages.codestyle.CodeStyle; import chalkbox.stages.conformance.Conformance; +import chalkbox.stages.conformance.ConformanceLite; import chalkbox.stages.mutation.Mutation; import chalkbox.stages.tlc.TLC; import org.github.gestalt.config.Gestalt; @@ -64,6 +65,10 @@ public Conformance toConformance() { return new Conformance(new ArrayList<>()); } + public ConformanceLite toConformanceLight() { + return new ConformanceLite(new ArrayList<>()); + } + public Functionality toFunctionality() throws ConfigException { try { return new Functionality( diff --git a/src/main/java/chalkbox/stages/conformance/ConformanceLite.java b/src/main/java/chalkbox/stages/conformance/ConformanceLite.java new file mode 100644 index 0000000..c46fd86 --- /dev/null +++ b/src/main/java/chalkbox/stages/conformance/ConformanceLite.java @@ -0,0 +1,147 @@ +package chalkbox.stages.conformance; + +import chalkbox.api.files.FileLoader; +import chalkbox.source.Solution; +import chalkbox.source.Source; +import chalkbox.source.Submission; +import chalkbox.stages.*; +import chalkbox.stages.conformance.comparator.ClassComparator; +import com.google.common.flogger.FluentLogger; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Checks whether a submission conforms at minimum to the specified public API. + * + * Detects missing files in a submission, compared to the expected + * file structure. Uses class comparators to identify methods and members in the + * submission that differ to those in the correct solution. + */ +public class ConformanceLite implements Stage { + private static final String name = "Conformance (Lite)"; + + private final List ignoreWildcards; + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** + * Sets up the conformance checker ready to check a submission. + */ + public ConformanceLite(List ignoreWildcards){ + this.ignoreWildcards = ignoreWildcards; + } + + @Override + public String getName() { + return name; + } + + @Override + public Type getType() { + return Type.SUBMISSION_AND_SOLUTION; + } + + @Override + public StageResult run(Submission submission) throws StageException { + return null; + } + + @Override + public StageResult run(Submission submission, List solutions) throws StageException { + return null; + } + + /** + * Runs the conformance stage against the provided submission. + * + * @param submission submission to check for conformance + * @return given submission with extra test result for conformance results + */ + public StageResult run(Submission submission, Solution solution) throws StageException { + var result = new Result(name).setVisibility(Visibility.HIDDEN).setOutputFormat("html"); + var missing = new ArrayList(); + var extra = new ArrayList(); + + try { + var compilation = submission.compileSrc(); + if (!compilation.success()) { + result.appendOutput("Submission did not compile, not checking conformance"); + result.appendOutput(compilation.output()); + return StageResult.fromOverview(result); + } + } catch (IOException e) { + result.appendOutput("Submission did not compile, not checking conformance"); + result.appendOutput(e.toString()); + return StageResult.fromOverview(result); + } + + try { + var compilation = solution.compileSrc(); + if (!compilation.success()) { + logger.atSevere().log(compilation.output()); + throw new StageException("Unable to compile solution"); + } + } catch (IOException e) { + throw new StageException(e.toString()); + } + + var actual = removeMatchingFiles(FileLoader.loadFiles(submission.getSrcFolder()), ignoreWildcards); + var expectedFiles = removeMatchingFiles(FileLoader.loadFiles(solution.getSrcFolder()), ignoreWildcards); + + for (var expected : expectedFiles) { + if (!actual.contains(expected)) { + missing.add(expected); + } + } + + // Enforce deterministic order of list of missing/extra files + Collections.sort(missing); + Collections.sort(extra); + + if (missing.isEmpty()) { + result.appendOutput("

✅ No missing files

"); + } else { + result.appendOutput(""" +

+ Warning: Having missing files will most likely cause an issue in latter stages. + """); + result.appendOutput("

Missing files

"); + result.appendOutput("
    "); + for (var missingFile : missing) { + result.appendOutput(String.format("
  • %s
  • ", missingFile)); + } + result.appendOutput("
"); + result.appendOutput("
"); + } + return StageResult.fromOverview(result); + } + + private List removeMatchingFiles(List files, List globs) { + var fs = FileSystems.getDefault(); + var filtered = new ArrayList(files); + var toBeRemoved = new ArrayList(); + for (var glob : globs) { + var matcher = fs.getPathMatcher(glob); + for (var file : files) { + if (!matcher.matches(Path.of(file))) { + toBeRemoved.add(file); + } + } + } + filtered.removeAll(toBeRemoved); + return filtered; + } +}