diff --git a/.github/workflows/complete-e2e.yml b/.github/workflows/complete-e2e.yml index 332cc834e1..40cec33bc8 100644 --- a/.github/workflows/complete-e2e.yml +++ b/.github/workflows/complete-e2e.yml @@ -43,7 +43,7 @@ jobs: node-version: "18" - name: Build Assembly - run: mvn -Pwith-report-viewer -DskipTests clean package assembly:single + run: mvn -DskipTests clean package assembly:single - name: Rename Jar run: mv cli/target/jplag-*-jar-with-dependencies.jar cli/target/jplag.jar diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 9629427c80..ab2bb989cd 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -7,6 +7,7 @@ on: - "**/pom.xml" - "**.java" - "**.g4" + - "report-viewer/**" pull_request: types: [opened, synchronize, reopened] paths: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3fcc4b7e3c..f71b7250c2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -38,6 +38,19 @@ jobs: with: node-version: "18" + - name: Set version of Report Viewer + shell: bash + run: | + VERSION=$(grep "" pom.xml | grep -oPm1 "(?<=)[^-|<]+") + MAJOR=$(echo $VERSION | cut -d '.' -f 1) + MINOR=$(echo $VERSION | cut -d '.' -f 2) + PATCH=$(echo $VERSION | cut -d '.' -f 3) + json=$(cat report-viewer/src/version.json) + json=$(echo "$json" | jq --arg MAJOR "$MAJOR" --arg MINOR "$MINOR" --arg PATCH "$PATCH" '.report_viewer_version |= { "major": $MAJOR | tonumber, "minor": $MINOR | tonumber, "patch": $PATCH | tonumber }') + echo "$json" > report-viewer/src/version.json + echo "Version of Report Viewer:" + cat report-viewer/src/version.json + - name: Build JPlag run: mvn -Pwith-report-viewer -U -B clean package assembly:single diff --git a/.github/workflows/report-viewer-demo.yml b/.github/workflows/report-viewer-demo.yml index f7a747813e..b6f0c02c2d 100644 --- a/.github/workflows/report-viewer-demo.yml +++ b/.github/workflows/report-viewer-demo.yml @@ -102,7 +102,7 @@ jobs: npm run build-demo - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.6.1 + uses: JamesIves/github-pages-deploy-action@v4.6.8 with: branch: gh-pages folder: report-viewer/dist diff --git a/.github/workflows/report-viewer-dev.yml b/.github/workflows/report-viewer-dev.yml index ef2bfd8074..c9e5b5a0fa 100644 --- a/.github/workflows/report-viewer-dev.yml +++ b/.github/workflows/report-viewer-dev.yml @@ -27,7 +27,7 @@ jobs: npm run build-dev - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.6.1 + uses: JamesIves/github-pages-deploy-action@v4.6.8 with: branch: gh-pages folder: report-viewer/dist diff --git a/.github/workflows/report-viewer.yml b/.github/workflows/report-viewer.yml deleted file mode 100644 index dfa0689e3d..0000000000 --- a/.github/workflows/report-viewer.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Report Viewer Deployment Workflow - -on: - workflow_dispatch: # Use this to dispatch from the Actions Tab - push: - branches: - - main - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout 🛎️ - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "18" - - - name: Set version of Report Viewer - shell: bash - run: | - VERSION=$(grep "" pom.xml | grep -oPm1 "(?<=)[^-|<]+") - MAJOR=$(echo $VERSION | cut -d '.' -f 1) - MINOR=$(echo $VERSION | cut -d '.' -f 2) - PATCH=$(echo $VERSION | cut -d '.' -f 3) - json=$(cat report-viewer/src/version.json) - json=$(echo "$json" | jq --arg MAJOR "$MAJOR" --arg MINOR "$MINOR" --arg PATCH "$PATCH" '.report_viewer_version |= { "major": $MAJOR | tonumber, "minor": $MINOR | tonumber, "patch": $PATCH | tonumber }') - echo "$json" > report-viewer/src/version.json - echo "Version of Report Viewer:" - cat report-viewer/src/version.json - - - name: Install and Build 🔧 - working-directory: report-viewer - run: | - npm install - npm run build-prod - - - name: Deploy 🚀 - uses: JamesIves/github-pages-deploy-action@v4.6.1 - with: - branch: gh-pages - folder: report-viewer/dist diff --git a/README.md b/README.md index 87d38e709a..a1ccf587e0 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Subsequence Match Merging --neighbor-length= Minimal length of neighboring matches to be merged (between 1 and minTokenMatch, default: 2). -Subcommands (supported languages): +Languages: c cpp csharp diff --git a/cli/pom.xml b/cli/pom.xml index f376667af7..fe351d8d08 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -183,7 +183,7 @@ org.codehaus.mojo exec-maven-plugin - 3.3.0 + 3.5.0 npm install diff --git a/cli/src/main/java/de/jplag/cli/CLI.java b/cli/src/main/java/de/jplag/cli/CLI.java index a5be417426..263a9021ed 100644 --- a/cli/src/main/java/de/jplag/cli/CLI.java +++ b/cli/src/main/java/de/jplag/cli/CLI.java @@ -49,6 +49,7 @@ public CLI(String[] args) { */ public void executeCli() throws ExitException, IOException { logger.debug("Your version of JPlag is {}", JPlag.JPLAG_VERSION); + JPlagVersionChecker.printVersionNotification(); if (!this.inputHandler.parse()) { CollectedLogger.setLogLevel(this.inputHandler.getCliOptions().advanced.logLevel); @@ -110,6 +111,7 @@ public File runJPlag() throws ExitException, FileNotFoundException { * @throws IOException If something went wrong with the internal server */ public void runViewer(File zipFile) throws IOException { + finalizeLogger(); // Prints the errors. The later finalizeLogger will print any errors logged after this point. JPlagRunner.runInternalServer(zipFile, this.inputHandler.getCliOptions().advanced.port); } diff --git a/cli/src/main/java/de/jplag/cli/JPlagRunner.java b/cli/src/main/java/de/jplag/cli/JPlagRunner.java index ee47fdba4a..3346eec627 100644 --- a/cli/src/main/java/de/jplag/cli/JPlagRunner.java +++ b/cli/src/main/java/de/jplag/cli/JPlagRunner.java @@ -43,7 +43,11 @@ public static void runInternalServer(File zipFile, int port) throws IOException ReportViewer reportViewer = new ReportViewer(zipFile, port); int actualPort = reportViewer.start(); logger.info("ReportViewer started on port http://localhost:{}", actualPort); - Desktop.getDesktop().browse(URI.create("http://localhost:" + actualPort + "/")); + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(URI.create("http://localhost:" + actualPort + "/")); + } else { + logger.info("Could not open browser. You can open the Report Viewer here: http://localhost:{}/", actualPort); + } System.out.println("Press Enter key to exit..."); System.in.read(); diff --git a/cli/src/main/java/de/jplag/cli/JPlagVersionChecker.java b/cli/src/main/java/de/jplag/cli/JPlagVersionChecker.java new file mode 100644 index 0000000000..2f8a3ce5cd --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/JPlagVersionChecker.java @@ -0,0 +1,91 @@ +package de.jplag.cli; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Optional; + +import javax.json.Json; +import javax.json.JsonArray; +import javax.json.JsonObject; +import javax.json.JsonReader; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.jplag.JPlag; +import de.jplag.reporting.reportobject.model.Version; + +/** + * Handles the check for newer versions. + */ +public class JPlagVersionChecker { + private static final String API_URL = "https://api.github.com/repos/jplag/JPlag/releases"; + private static final Logger logger = LoggerFactory.getLogger(JPlagVersionChecker.class); + private static final String EXPECTED_VERSION_FORMAT = "v\\d\\.\\d\\.\\d+"; + private static final String WARNING_UNABLE_TO_FETCH = "Unable to fetch version information. New version notification will not work."; + private static final String NEWER_VERSION_AVAILABLE = "There is a newer version ({}) available. You can download the newest version here: https://github.com/jplag/JPlag/releases"; + private static final String UNEXPECTED_ERROR = "There was an unexpected error, when checking for new versions. Please report this on: https://github.com/jplag/JPlag/issues"; + + private JPlagVersionChecker() { + + } + + /** + * Prints a warning if a newer version is available on GitHub. + */ + public static void printVersionNotification() { + Optional newerVersion = checkForNewVersion(); + newerVersion.ifPresent(version -> logger.warn(NEWER_VERSION_AVAILABLE, version)); + } + + private static Optional checkForNewVersion() { + try { + JsonArray array = fetchApi(); + Version newest = getNewestVersion(array); + Version current = JPlag.JPLAG_VERSION; + + if (newest.compareTo(current) > 0) { + return Optional.of(newest); + } + } catch (IOException | URISyntaxException e) { + logger.info(WARNING_UNABLE_TO_FETCH); + } catch (Exception e) { + logger.warn(UNEXPECTED_ERROR, e); + } + + return Optional.empty(); + } + + private static JsonArray fetchApi() throws IOException, URISyntaxException { + URL url = new URI(API_URL).toURL(); + URLConnection connection = url.openConnection(); + + try (JsonReader reader = Json.createReader(connection.getInputStream())) { + return reader.readArray(); + } + } + + private static Version getNewestVersion(JsonArray apiResult) { + return apiResult.stream().map(JsonObject.class::cast).map(version -> version.getString("name")) + .filter(versionName -> versionName.matches(EXPECTED_VERSION_FORMAT)).limit(1).map(JPlagVersionChecker::parseVersion).findFirst() + .orElse(JPlag.JPLAG_VERSION); + } + + /** + * Parses the version name. + * @param versionName The version name. The expected format is: v[major].[minor].[patch] + * @return The parsed version + */ + private static Version parseVersion(String versionName) { + String withoutPrefix = versionName.substring(1); + String[] parts = withoutPrefix.split("\\."); + return parseVersionParts(parts); + } + + private static Version parseVersionParts(String[] parts) { + return new Version(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])); + } +} diff --git a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java index b93e8cacf7..54fdb9d304 100644 --- a/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java +++ b/cli/src/main/java/de/jplag/cli/logger/CollectedLogger.java @@ -1,5 +1,6 @@ package de.jplag.cli.logger; +import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -19,7 +20,6 @@ public class CollectedLogger extends AbstractLogger { private static final String JPLAG_LOGGER_PREFIX = "de.jplag."; private static final Level LOG_LEVEL_FOR_EXTERNAL_LIBRARIES = Level.ERROR; private static final int MAXIMUM_MESSAGE_LENGTH = 32; - private static final PrintStream TARGET_STREAM = System.out; private static Level currentLogLevel = Level.INFO; private final transient SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-hh:mm:ss_SSS"); @@ -148,11 +148,10 @@ private StringBuilder prepareLogOutput(LogEntry entry) { private void printLogEntry(LogEntry entry) { StringBuilder output = prepareLogOutput(entry); - TARGET_STREAM.println(output); + DelayablePrinter.getInstance().println(output.toString()); if (entry.cause() != null) { - entry.cause().printStackTrace(TARGET_STREAM); + this.printStackTrace(entry.cause()); } - TARGET_STREAM.flush(); } public static Level getLogLevel() { @@ -162,4 +161,11 @@ public static Level getLogLevel() { public static void setLogLevel(Level logLevel) { currentLogLevel = logLevel; } + + private void printStackTrace(Throwable error) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + error.printStackTrace(new PrintStream(outputStream)); + String stackTrace = outputStream.toString(); + DelayablePrinter.getInstance().println(stackTrace); + } } diff --git a/cli/src/main/java/de/jplag/cli/logger/DelayablePrinter.java b/cli/src/main/java/de/jplag/cli/logger/DelayablePrinter.java new file mode 100644 index 0000000000..f8b1d5fcee --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/logger/DelayablePrinter.java @@ -0,0 +1,73 @@ +package de.jplag.cli.logger; + +import java.io.PrintStream; +import java.util.PriorityQueue; +import java.util.Queue; + +/** + * Prints strings to stdout. Provides the option to delay the actual printing. + */ +public class DelayablePrinter { + private final Queue outputQueue; + private PrintStream targetStream; + + private boolean isDelayed; + + private static final class InstanceHolder { + private static final DelayablePrinter instance = new DelayablePrinter(); + } + + /** + * Threadsafe singleton getter + * @return The singleton instance + */ + public static DelayablePrinter getInstance() { + return InstanceHolder.instance; + } + + private DelayablePrinter() { + this.outputQueue = new PriorityQueue<>(); + this.targetStream = System.out; + this.isDelayed = false; + } + + /** + * Prints the given string to the terminal appending a line-break + * @param output The string to print + */ + public synchronized void println(String output) { + this.outputQueue.offer(output); + this.printQueue(); + } + + /** + * Stops printing to the terminal until {@link #resume()} is called + */ + public synchronized void delay() { + this.isDelayed = true; + } + + /** + * Resumes printing if {@link #delay()} was called + */ + public synchronized void resume() { + this.isDelayed = false; + this.printQueue(); + } + + /** + * Changes the output stream messages are written to + */ + public void setOutputStream(PrintStream printStream) { + this.targetStream = printStream; + } + + private synchronized void printQueue() { + if (!this.isDelayed) { + while (!this.outputQueue.isEmpty()) { + this.targetStream.println(this.outputQueue.poll()); + } + this.targetStream.flush(); + } + } +} diff --git a/cli/src/main/java/de/jplag/cli/logger/IdleBar.java b/cli/src/main/java/de/jplag/cli/logger/IdleBar.java index e5b7f0d3da..c9aee2da8c 100644 --- a/cli/src/main/java/de/jplag/cli/logger/IdleBar.java +++ b/cli/src/main/java/de/jplag/cli/logger/IdleBar.java @@ -7,12 +7,10 @@ import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; -import de.jplag.logging.ProgressBar; - /** * Prints an idle progress bar, that does not count upwards. */ -public class IdleBar implements ProgressBar { +public class IdleBar extends LogDelayingProgressBar { private final PrintStream output; private final Thread runner; @@ -27,6 +25,7 @@ public class IdleBar implements ProgressBar { private boolean running = false; public IdleBar(String text) { + super(); this.output = System.out; this.runner = new Thread(this::run); this.length = 50; @@ -61,6 +60,7 @@ public void dispose() { } this.output.print('\r'); this.output.println(this.text + ": complete"); + super.dispose(); } private void run() { diff --git a/cli/src/main/java/de/jplag/cli/logger/LogDelayingProgressBar.java b/cli/src/main/java/de/jplag/cli/logger/LogDelayingProgressBar.java new file mode 100644 index 0000000000..b572086f86 --- /dev/null +++ b/cli/src/main/java/de/jplag/cli/logger/LogDelayingProgressBar.java @@ -0,0 +1,17 @@ +package de.jplag.cli.logger; + +import de.jplag.logging.ProgressBar; + +/** + * Superclass for progress bars, that delay the log output until the bar is done + */ +public abstract class LogDelayingProgressBar implements ProgressBar { + protected LogDelayingProgressBar() { + DelayablePrinter.getInstance().delay(); + } + + @Override + public void dispose() { + DelayablePrinter.getInstance().resume(); + } +} diff --git a/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java b/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java index 4305a497e0..d50ed1f181 100644 --- a/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java +++ b/cli/src/main/java/de/jplag/cli/logger/TongfeiProgressBar.java @@ -1,14 +1,13 @@ package de.jplag.cli.logger; -import de.jplag.logging.ProgressBar; - /** * A ProgressBar, that used the tongfei progress bar library underneath, to show progress bars on the cli. */ -public class TongfeiProgressBar implements ProgressBar { +public class TongfeiProgressBar extends LogDelayingProgressBar { private final me.tongfei.progressbar.ProgressBar progressBar; public TongfeiProgressBar(me.tongfei.progressbar.ProgressBar progressBar) { + super(); this.progressBar = progressBar; } @@ -20,5 +19,6 @@ public void step(int number) { @Override public void dispose() { this.progressBar.close(); + super.dispose(); } } diff --git a/cli/src/main/java/de/jplag/cli/options/CliOptions.java b/cli/src/main/java/de/jplag/cli/options/CliOptions.java index 73f95f91bc..748a8f6c8b 100644 --- a/cli/src/main/java/de/jplag/cli/options/CliOptions.java +++ b/cli/src/main/java/de/jplag/cli/options/CliOptions.java @@ -54,7 +54,7 @@ public class CliOptions implements Runnable { public String resultFile = "results"; @Option(names = {"-M", "--mode"}, description = "The mode of JPlag. One of: ${COMPLETION-CANDIDATES} (default: ${DEFAULT_VALUE})") - public JPlagMode mode = JPlagMode.RUN; + public JPlagMode mode = JPlagMode.RUN_AND_VIEW; @Option(names = {"--normalize"}, description = "Activate the normalization of tokens. Supported for languages: Java, C++.") public boolean normalize = false; diff --git a/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java index 77389f3f39..4bc388a35d 100644 --- a/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java +++ b/cli/src/main/java/de/jplag/cli/picocli/CliInputHandler.java @@ -1,5 +1,6 @@ package de.jplag.cli.picocli; +import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_DESCRIPTION_HEADING; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_OPTION_LIST; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_SYNOPSIS; @@ -70,6 +71,7 @@ private CommandLine buildCommandLine() { } return it; }).collect(Collectors.joining(System.lineSeparator())) + System.lineSeparator()); + cli.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST_HEADING, help -> "Languages:" + System.lineSeparator()); buildSubcommands().forEach(cli::addSubcommand); diff --git a/cli/src/main/java/de/jplag/cli/server/ReportViewer.java b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java index 09d88857f8..32be964853 100644 --- a/cli/src/main/java/de/jplag/cli/server/ReportViewer.java +++ b/cli/src/main/java/de/jplag/cli/server/ReportViewer.java @@ -59,12 +59,14 @@ public int start() throws IOException { throw new IllegalStateException("Server already started"); } + System.setProperty("java.net.preferIPv4Stack", "true"); + int currentPort = this.port; int remainingLookups = MAX_PORT_LOOKUPS; BindException lastException = new BindException("Could not create server. Probably due to no free port found."); while (server == null && remainingLookups-- > 0) { try { - server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), currentPort), 0); + server = HttpServer.create(new InetSocketAddress(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}), currentPort), 0); } catch (BindException e) { logger.info("Port {} is not available. Trying to find a different one.", currentPort); lastException = e; diff --git a/cli/src/test/java/de/jplag/cli/DebugTest.java b/cli/src/test/java/de/jplag/cli/DebugTest.java new file mode 100644 index 0000000000..a6f22f74c0 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/DebugTest.java @@ -0,0 +1,27 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class DebugTest extends CliTest { + @Test + void testDefaultDebug() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertFalse(options.debugParser()); + } + + @Test + void testSetDebug() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.DEBUG, true)); + assertTrue(options.debugParser()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/ExcludeFileTest.java b/cli/src/test/java/de/jplag/cli/ExcludeFileTest.java new file mode 100644 index 0000000000..e2cb116699 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/ExcludeFileTest.java @@ -0,0 +1,29 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class ExcludeFileTest extends CliTest { + private static final String EXCLUDE_FILE_NAME = "exclusions"; + + @Test + void testNoDefaultExcludeFile() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertNull(options.exclusionFileName()); + } + + @Test + void testSetExcludeFile() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.EXCLUDE_FILES, EXCLUDE_FILE_NAME)); + assertEquals(EXCLUDE_FILE_NAME, options.exclusionFileName()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/LogLevelTest.java b/cli/src/test/java/de/jplag/cli/LogLevelTest.java new file mode 100644 index 0000000000..9994de5fa9 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/LogLevelTest.java @@ -0,0 +1,28 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.slf4j.event.Level; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; + +class LogLevelTest extends CliTest { + private static final Level DEFAULT_LOG_LEVEL = Level.INFO; + + @Test + void testDefaultLogLevel() throws IOException, ExitException { + Level level = runCliForLogLevel(); + assertEquals(DEFAULT_LOG_LEVEL, level); + } + + @Test + void testSetLogLevel() throws IOException, ExitException { + Level level = runCliForLogLevel(args -> args.with(CliArgument.LOG_LEVEL, Level.ERROR.name())); + assertEquals(Level.ERROR, level); + } +} diff --git a/cli/src/test/java/de/jplag/cli/ResultFileTest.java b/cli/src/test/java/de/jplag/cli/ResultFileTest.java new file mode 100644 index 0000000000..888d166fd3 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/ResultFileTest.java @@ -0,0 +1,68 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; + +class ResultFileTest extends CliTest { + private static final String DEFAULT_RESULT_FILE = "results.zip"; + private static final String TEST_RESULT_FILE = "customResults.zip"; + private static final String TEST_RESULT_FILE_WITH_AVOIDANCE = "customResults(1).zip"; + private static final String TEST_RESULT_FILE_WITHOUT_ZIP = "customResults"; + + @Test + void testDefaultResultFolder() throws IOException, ExitException { + String targetPath = runCliForTargetPath(); + assertEquals(DEFAULT_RESULT_FILE, targetPath); + } + + @Test + void testSetResultFolder() throws IOException, ExitException { + String targetPath = runCliForTargetPath(args -> args.with(CliArgument.RESULT_FILE, TEST_RESULT_FILE)); + assertEquals(TEST_RESULT_FILE, targetPath); + } + + @Test + void testSetResultFolderWithoutZip() throws IOException, ExitException { + String targetPath = runCliForTargetPath(args -> args.with(CliArgument.RESULT_FILE, TEST_RESULT_FILE_WITHOUT_ZIP)); + assertEquals(TEST_RESULT_FILE, targetPath); + } + + @Test + void testResultFileOverrideAvoidance() throws IOException, ExitException { + File testDir = Files.createTempDirectory("JPlagResultFileTest").toFile(); + File targetFile = new File(testDir, TEST_RESULT_FILE); + File expectedTargetFile = new File(testDir, TEST_RESULT_FILE_WITH_AVOIDANCE); + targetFile.createNewFile(); + + String actualTargetPath = runCliForTargetPath(args -> args.with(CliArgument.RESULT_FILE, targetFile.getAbsolutePath())); + + targetFile.delete(); + testDir.delete(); + + assertEquals(expectedTargetFile.getAbsolutePath(), actualTargetPath); + } + + @Test + void testResultFileOverwrite() throws IOException, ExitException { + File testDir = Files.createTempDirectory("JPlagResultFileTest").toFile(); + File targetFile = new File(testDir, TEST_RESULT_FILE); + targetFile.createNewFile(); + + String actualTargetPath = runCliForTargetPath( + args -> args.with(CliArgument.RESULT_FILE, targetFile.getAbsolutePath()).with(CliArgument.OVERWRITE_RESULT_FILE, true)); + + targetFile.delete(); + testDir.delete(); + + assertEquals(targetFile.getAbsolutePath(), actualTargetPath); + } +} diff --git a/cli/src/test/java/de/jplag/cli/SubdirectoryTest.java b/cli/src/test/java/de/jplag/cli/SubdirectoryTest.java new file mode 100644 index 0000000000..e2d319551f --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/SubdirectoryTest.java @@ -0,0 +1,29 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class SubdirectoryTest extends CliTest { + private static final String TEST_SUBDIRECTORY = "dir"; + + @Test + void testDefaultSubdirectory() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertNull(options.subdirectoryName()); + } + + @Test + void testSetSubdirectory() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.SUBDIRECTORY, TEST_SUBDIRECTORY)); + assertEquals(TEST_SUBDIRECTORY, options.subdirectoryName()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/SuffixesTest.java b/cli/src/test/java/de/jplag/cli/SuffixesTest.java new file mode 100644 index 0000000000..ab095224b9 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/SuffixesTest.java @@ -0,0 +1,30 @@ +package de.jplag.cli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import de.jplag.cli.test.CliArgument; +import de.jplag.cli.test.CliTest; +import de.jplag.exceptions.ExitException; +import de.jplag.options.JPlagOptions; + +class SuffixesTest extends CliTest { + private static final List JAVA_SUFFIXES = List.of(".java", ".JAVA"); + private static final List CUSTOM_SUFFIXES = List.of(".j", ".jva"); + + @Test + void testDefaultSuffixes() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(); + assertEquals(JAVA_SUFFIXES, options.fileSuffixes()); + } + + @Test + void testSetSuffixes() throws IOException, ExitException { + JPlagOptions options = runCliForOptions(args -> args.with(CliArgument.SUFFIXES, CUSTOM_SUFFIXES.toArray(new String[0]))); + assertEquals(CUSTOM_SUFFIXES, options.fileSuffixes()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/logger/DelayablePrinterTest.java b/cli/src/test/java/de/jplag/cli/logger/DelayablePrinterTest.java new file mode 100644 index 0000000000..540f9ca083 --- /dev/null +++ b/cli/src/test/java/de/jplag/cli/logger/DelayablePrinterTest.java @@ -0,0 +1,51 @@ +package de.jplag.cli.logger; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class DelayablePrinterTest { + private static final String TEST_MESSAGE = "Hello World"; + + private static ByteArrayOutputStream outputStream; + + @BeforeAll + static void setUp() { + outputStream = new ByteArrayOutputStream(); + DelayablePrinter.getInstance().setOutputStream(new PrintStream(outputStream)); + } + + @AfterAll + static void tearDown() { + DelayablePrinter.getInstance().setOutputStream(System.out); + } + + @AfterEach + void cleanUpAfterTest() { + DelayablePrinter.getInstance().resume(); + outputStream.reset(); + } + + @Test + void testDelay() { + DelayablePrinter.getInstance().delay(); + DelayablePrinter.getInstance().println(TEST_MESSAGE); + + Assertions.assertEquals("", outputStream.toString()); + + DelayablePrinter.getInstance().resume(); + + Assertions.assertEquals(TEST_MESSAGE + System.lineSeparator(), outputStream.toString()); + } + + @Test + void testDirectPrinting() { + DelayablePrinter.getInstance().println(TEST_MESSAGE); + Assertions.assertEquals(TEST_MESSAGE + System.lineSeparator(), outputStream.toString()); + } +} diff --git a/cli/src/test/java/de/jplag/cli/test/CliArgument.java b/cli/src/test/java/de/jplag/cli/test/CliArgument.java index 6ac22a524c..3b90977ab4 100644 --- a/cli/src/test/java/de/jplag/cli/test/CliArgument.java +++ b/cli/src/test/java/de/jplag/cli/test/CliArgument.java @@ -28,4 +28,10 @@ public record CliArgument(String name, boolean isPositional) { public static CliArgument RESULT_FILE = new CliArgument<>("r", false); public static CliArgument OVERWRITE_RESULT_FILE = new CliArgument<>("overwrite", false); + + public static CliArgument LOG_LEVEL = new CliArgument<>("log-level", false); + public static CliArgument DEBUG = new CliArgument<>("d", false); + + public static CliArgument SUBDIRECTORY = new CliArgument<>("subdirectory", false); + public static CliArgument EXCLUDE_FILES = new CliArgument<>("x", false); } diff --git a/cli/src/test/java/de/jplag/cli/test/CliResult.java b/cli/src/test/java/de/jplag/cli/test/CliResult.java index 4978a5195e..7051a6061f 100644 --- a/cli/src/test/java/de/jplag/cli/test/CliResult.java +++ b/cli/src/test/java/de/jplag/cli/test/CliResult.java @@ -1,6 +1,8 @@ package de.jplag.cli.test; +import org.slf4j.event.Level; + import de.jplag.options.JPlagOptions; -public record CliResult(JPlagOptions jPlagOptions, String targetPath) { +public record CliResult(JPlagOptions jPlagOptions, String targetPath, Level logLevel) { } diff --git a/cli/src/test/java/de/jplag/cli/test/CliTest.java b/cli/src/test/java/de/jplag/cli/test/CliTest.java index d26a1fb477..bff7528a60 100644 --- a/cli/src/test/java/de/jplag/cli/test/CliTest.java +++ b/cli/src/test/java/de/jplag/cli/test/CliTest.java @@ -11,9 +11,11 @@ import org.junit.jupiter.api.BeforeEach; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.slf4j.event.Level; import de.jplag.JPlagResult; import de.jplag.cli.*; +import de.jplag.cli.logger.CollectedLogger; import de.jplag.cli.picocli.CliInputHandler; import de.jplag.exceptions.ExitException; import de.jplag.options.JPlagOptions; @@ -105,6 +107,29 @@ protected String runCliForTargetPath(Consumer additionalOpti return runCli(additionalOptionsBuilder).targetPath(); } + /** + * Runs the cli + * @return The log level set by the cli + * @throws ExitException If JPlag throws an exception + * @throws IOException If JPlag throws an exception + * @see #runCli() + */ + protected Level runCliForLogLevel() throws IOException, ExitException { + return runCli().logLevel(); + } + + /** + * Runs the cli using custom options + * @param additionalOptionsBuilder May modify the {@link CliArgumentBuilder} object to set custom options for this run. + * @return The log level set by the cli + * @throws ExitException If JPlag throws an exception + * @throws IOException If JPlag throws an exception + * @see #runCli() + */ + protected Level runCliForLogLevel(Consumer additionalOptionsBuilder) throws IOException, ExitException { + return runCli(additionalOptionsBuilder).logLevel(); + } + /** * Runs the cli * @return The options returned by the cli @@ -129,7 +154,7 @@ protected CliResult runCli(Consumer additionalOptionsBuilder String targetPath = (String) getWritableFileMethod.invoke(cli); - return new CliResult(optionsBuilder.buildOptions(), targetPath); + return new CliResult(optionsBuilder.buildOptions(), targetPath, CollectedLogger.getLogLevel()); } catch (IllegalAccessException | InvocationTargetException e) { Assumptions.abort("Could not access private field in CLI for test."); return null; // will not be executed diff --git a/core/pom.xml b/core/pom.xml index 0d1f9480cd..ce0214dc22 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -37,6 +37,12 @@ ${revision} test + + io.soabase.record-builder + record-builder-processor + 43 + provided + @@ -46,5 +52,15 @@ src/main/resources + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.10.1 + + src/main/java;target/generated-sources/annotations + + + diff --git a/core/src/main/java/de/jplag/Submission.java b/core/src/main/java/de/jplag/Submission.java index 92c8fd5c8d..5610a19873 100644 --- a/core/src/main/java/de/jplag/Submission.java +++ b/core/src/main/java/de/jplag/Submission.java @@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory; import de.jplag.exceptions.LanguageException; -import de.jplag.normalization.TokenStringNormalizer; +import de.jplag.normalization.TokenSequenceNormalizer; import de.jplag.options.JPlagOptions; /** @@ -259,7 +259,7 @@ private static File createErrorDirectory(String... subdirectoryNames) { */ void normalize() { List originalOrder = getOrder(tokenList); - tokenList = TokenStringNormalizer.normalize(tokenList); + tokenList = TokenSequenceNormalizer.normalize(tokenList); List normalizedOrder = getOrder(tokenList); logger.debug("original line order: {}", originalOrder); diff --git a/core/src/main/java/de/jplag/SubmissionSet.java b/core/src/main/java/de/jplag/SubmissionSet.java index f7c1438bbb..884d800450 100644 --- a/core/src/main/java/de/jplag/SubmissionSet.java +++ b/core/src/main/java/de/jplag/SubmissionSet.java @@ -99,6 +99,11 @@ public List getInvalidSubmissions() { return invalidSubmissions; } + /** + * Normalizes the token sequences of all submissions (including basecode). This makes the token sequence invariant to + * dead code insertion and independent statement reordering by removing dead tokens and optionally reordering tokens to + * a deterministic order. + */ public void normalizeSubmissions() { if (baseCodeSubmission != null) { baseCodeSubmission.normalize(); diff --git a/core/src/main/java/de/jplag/normalization/MultipleEdge.java b/core/src/main/java/de/jplag/normalization/MultipleEdge.java index b10fda2ea5..732d3d3cf2 100644 --- a/core/src/main/java/de/jplag/normalization/MultipleEdge.java +++ b/core/src/main/java/de/jplag/normalization/MultipleEdge.java @@ -6,7 +6,7 @@ import de.jplag.semantics.Variable; /** - * Models a multiple edge in the normalization graph. Contains multiple edges. + * Models multiple edges between two nodes in the normalization graph. */ class MultipleEdge { private final Set edges; diff --git a/core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java b/core/src/main/java/de/jplag/normalization/NormalizationGraph.java similarity index 78% rename from core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java rename to core/src/main/java/de/jplag/normalization/NormalizationGraph.java index fc995e69d7..e07c873b50 100644 --- a/core/src/main/java/de/jplag/normalization/NormalizationGraphConstructor.java +++ b/core/src/main/java/de/jplag/normalization/NormalizationGraph.java @@ -14,21 +14,28 @@ import de.jplag.semantics.Variable; /** - * Constructs the normalization graph. + * Token normalization graph, which is a directed graph based on nodes of type {@link Statement} and edges of type + * {@link MultipleEdge}. This class class inherits from {@link SimpleDirectedGraph} to provide a data structure for the + * token sequence normalization. */ -class NormalizationGraphConstructor { - private final SimpleDirectedGraph graph; +public class NormalizationGraph extends SimpleDirectedGraph { + + private static final long serialVersionUID = -8407465274643809647L; // generated + private int bidirectionalBlockDepth; - private final Collection fullPositionSignificanceIncoming; - private Statement lastFullPositionSignificance; - private Statement lastPartialPositionSignificance; - private final Map> variableReads; - private final Map> variableWrites; - private final Set inCurrentBidirectionalBlock; - private Statement current; - - NormalizationGraphConstructor(List tokens) { - graph = new SimpleDirectedGraph<>(MultipleEdge.class); + private final transient Collection fullPositionSignificanceIncoming; + private transient Statement lastFullPositionSignificance; + private transient Statement lastPartialPositionSignificance; + private final transient Map> variableReads; + private final transient Map> variableWrites; + private final transient Set inCurrentBidirectionalBlock; + private transient Statement current; + + /** + * Creates a new normalization graph. + */ + public NormalizationGraph(List tokens) { + super(MultipleEdge.class); bidirectionalBlockDepth = 0; fullPositionSignificanceIncoming = new ArrayList<>(); variableReads = new HashMap<>(); @@ -45,12 +52,8 @@ class NormalizationGraphConstructor { addStatement(builderForCurrent.build()); } - SimpleDirectedGraph get() { - return graph; - } - private void addStatement(Statement statement) { - graph.addVertex(statement); + addVertex(statement); this.current = statement; processBidirectionalBlock(); processFullPositionSignificance(); @@ -123,10 +126,10 @@ private void processWrites() { * @param cause the variable that caused the edge, may be null */ private void addIncomingEdgeToCurrent(Statement start, EdgeType type, Variable cause) { - MultipleEdge multipleEdge = graph.getEdge(start, current); + MultipleEdge multipleEdge = getEdge(start, current); if (multipleEdge == null) { multipleEdge = new MultipleEdge(); - graph.addEdge(start, current, multipleEdge); + addEdge(start, current, multipleEdge); } multipleEdge.addEdge(type, cause); } @@ -135,4 +138,5 @@ private void addVariableToMap(Map> variableMap, variableMap.putIfAbsent(variable, new ArrayList<>()); variableMap.get(variable).add(current); } + } diff --git a/core/src/main/java/de/jplag/normalization/Statement.java b/core/src/main/java/de/jplag/normalization/Statement.java index a749a57740..81f9b33640 100644 --- a/core/src/main/java/de/jplag/normalization/Statement.java +++ b/core/src/main/java/de/jplag/normalization/Statement.java @@ -8,7 +8,7 @@ import de.jplag.semantics.CodeSemantics; /** - * Models statements, which are the nodes of the normalization graph. + * Models statements, which are the nodes of the normalization graph. A statement refers to one or more tokens. */ class Statement implements Comparable { @@ -16,6 +16,11 @@ class Statement implements Comparable { private final int lineNumber; private final CodeSemantics semantics; + /** + * Constructs a new Statement. + * @param tokens the list of tokens that represent this statement. + * @param lineNumber the line number where this statement occurs in the source code. + */ Statement(List tokens, int lineNumber) { this.tokens = Collections.unmodifiableList(tokens); this.lineNumber = lineNumber; @@ -30,8 +35,8 @@ CodeSemantics semantics() { return semantics; } - void markKeep() { - semantics.markKeep(); + void markAsCritical() { + semantics.markAsCritical(); } private int tokenOrdinal(Token token) { diff --git a/core/src/main/java/de/jplag/normalization/StatementBuilder.java b/core/src/main/java/de/jplag/normalization/StatementBuilder.java index eef5d0c821..f9f3bd5008 100644 --- a/core/src/main/java/de/jplag/normalization/StatementBuilder.java +++ b/core/src/main/java/de/jplag/normalization/StatementBuilder.java @@ -13,6 +13,10 @@ class StatementBuilder { private final List tokens; private final int lineNumber; + /** + * Constructs a new StatementBuilder. + * @param lineNumber the line number where the statement starts in the source code. + */ StatementBuilder(int lineNumber) { this.lineNumber = lineNumber; this.tokens = new ArrayList<>(); diff --git a/core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java b/core/src/main/java/de/jplag/normalization/TokenSequenceNormalizer.java similarity index 55% rename from core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java rename to core/src/main/java/de/jplag/normalization/TokenSequenceNormalizer.java index 8ffafffbf7..9a1256300e 100644 --- a/core/src/main/java/de/jplag/normalization/TokenStringNormalizer.java +++ b/core/src/main/java/de/jplag/normalization/TokenSequenceNormalizer.java @@ -1,7 +1,6 @@ package de.jplag.normalization; import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.PriorityQueue; @@ -9,29 +8,35 @@ import java.util.stream.Collectors; import org.jgrapht.Graphs; -import org.jgrapht.graph.SimpleDirectedGraph; import de.jplag.Token; /** * Performs token sequence normalization. */ -public class TokenStringNormalizer { +public final class TokenSequenceNormalizer { - private TokenStringNormalizer() { + private TokenSequenceNormalizer() { + // private constructor for non-instantiability. } /** * Performs token sequence normalization. Tokens representing dead code have been eliminated and tokens representing - * subsequent independent statements have been put in a fixed order. Works by first constructing a Normalization Graph - * and then turning it back into a token sequence. + * subsequent independent statements have been put in a fixed order if sorting is true. Works by first constructing a + * Normalization Graph and then turning it back into a token sequence. For more information refer to the + * corresponding paper * @param tokens The original token sequence, remains unaltered. - * @return The normalized token sequence as unmodifiable list. + * @return The normalized token sequence. */ public static List normalize(List tokens) { - SimpleDirectedGraph normalizationGraph = new NormalizationGraphConstructor(tokens).get(); + NormalizationGraph graph = new NormalizationGraph(tokens); + propagateCriticalityStatus(graph); + return normalizeWithSorting(tokens, graph); + } + + // Add tokens in normalized original order, removing dead tokens + private static List normalizeWithSorting(List tokens, NormalizationGraph normalizationGraph) { List normalizedTokens = new ArrayList<>(tokens.size()); - spreadKeep(normalizationGraph); PriorityQueue roots = normalizationGraph.vertexSet().stream() // .filter(v -> !Graphs.vertexHasPredecessors(normalizationGraph, v)) // .collect(Collectors.toCollection(PriorityQueue::new)); @@ -39,7 +44,7 @@ public static List normalize(List tokens) { PriorityQueue newRoots = new PriorityQueue<>(); do { Statement statement = roots.poll(); - if (statement.semantics().keep()) { + if (statement.semantics().isCritical()) { normalizedTokens.addAll(statement.tokens()); } for (Statement successor : Graphs.successorListOf(normalizationGraph, statement)) { @@ -51,26 +56,29 @@ public static List normalize(List tokens) { } while (!roots.isEmpty()); roots = newRoots; } - return Collections.unmodifiableList(normalizedTokens); + return normalizedTokens; } /** - * Spread keep status to every node that does not represent dead code. Nodes without keep status are later eliminated. + * Spread criticality status to every node that does not represent dead code. Nodes without keep criticality are later + * eliminated (dead nodes). Before calling this method, only the statements that directly affect the behavior are marked + * as critical. After calling this method, this also holds true for statement that (transitively) depend (read/write) on + * the critical ones. */ - private static void spreadKeep(SimpleDirectedGraph normalizationGraph) { + private static void propagateCriticalityStatus(NormalizationGraph normalizationGraph) { Queue visit = new LinkedList<>(normalizationGraph.vertexSet().stream() // - .filter(tl -> tl.semantics().keep()).toList()); + .filter(tl -> tl.semantics().isCritical()).toList()); while (!visit.isEmpty()) { Statement current = visit.remove(); for (Statement predecessor : Graphs.predecessorListOf(normalizationGraph, current)) { // performance of iteration? - if (!predecessor.semantics().keep() && normalizationGraph.getEdge(predecessor, current).isVariableFlow()) { - predecessor.markKeep(); + if (!predecessor.semantics().isCritical() && normalizationGraph.getEdge(predecessor, current).isVariableFlow()) { + predecessor.markAsCritical(); visit.add(predecessor); } } for (Statement successor : Graphs.successorListOf(normalizationGraph, current)) { - if (!successor.semantics().keep() && normalizationGraph.getEdge(current, successor).isVariableReverseFlow()) { - successor.markKeep(); + if (!successor.semantics().isCritical() && normalizationGraph.getEdge(current, successor).isVariableReverseFlow()) { + successor.markAsCritical(); visit.add(successor); } } diff --git a/core/src/main/java/de/jplag/options/JPlagOptions.java b/core/src/main/java/de/jplag/options/JPlagOptions.java index 7dd35c0660..2a27e53c6c 100644 --- a/core/src/main/java/de/jplag/options/JPlagOptions.java +++ b/core/src/main/java/de/jplag/options/JPlagOptions.java @@ -3,8 +3,6 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -25,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.soabase.recordbuilder.core.RecordBuilder; /** * This record defines the options to configure {@link JPlag}. @@ -49,6 +48,7 @@ * @param clusteringOptions Clustering options * @param debugParser If true, submissions that cannot be parsed will be stored in a separate directory. */ +@RecordBuilder() public record JPlagOptions(@JsonSerialize(using = LanguageSerializer.class) Language language, @JsonProperty("min_token_match") Integer minimumTokenMatch, @JsonProperty("submission_directories") Set submissionDirectories, @JsonProperty("old_directories") Set oldSubmissionDirectories, @JsonProperty("base_directory") File baseCodeSubmissionDirectory, @@ -56,15 +56,24 @@ public record JPlagOptions(@JsonSerialize(using = LanguageSerializer.class) Lang @JsonProperty("exclusion_file_name") String exclusionFileName, @JsonProperty("similarity_metric") SimilarityMetric similarityMetric, @JsonProperty("similarity_threshold") double similarityThreshold, @JsonProperty("max_comparisons") int maximumNumberOfComparisons, @JsonProperty("cluster") ClusteringOptions clusteringOptions, boolean debugParser, @JsonProperty("merging") MergingOptions mergingOptions, - @JsonProperty("normalize") boolean normalize) { + @JsonProperty("normalize") boolean normalize) implements JPlagOptionsBuilder.With { public static final double DEFAULT_SIMILARITY_THRESHOLD = 0; - public static final int DEFAULT_SHOWN_COMPARISONS = 500; + public static final int DEFAULT_SHOWN_COMPARISONS = 2500; public static final int SHOW_ALL_COMPARISONS = 0; public static final SimilarityMetric DEFAULT_SIMILARITY_METRIC = SimilarityMetric.AVG; - public static final Charset CHARSET = StandardCharsets.UTF_8; public static final String ERROR_FOLDER = "errors"; + /** + * @param lang The new language + * @return The modified options + * @deprecated Use withLanguage instead + */ + @Deprecated(forRemoval = true) + public JPlagOptions withLanguageOption(Language lang) { + return this.withLanguage(lang); + } + private static final Logger logger = LoggerFactory.getLogger(JPlagOptions.class); public JPlagOptions(Language language, Set submissionDirectories, Set oldSubmissionDirectories) { @@ -93,96 +102,6 @@ public JPlagOptions(Language language, Integer minimumTokenMatch, Set subm this.normalize = normalize; } - public JPlagOptions withLanguageOption(Language language) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withDebugParser(boolean debugParser) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withFileSuffixes(List fileSuffixes) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withSimilarityThreshold(double similarityThreshold) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withMaximumNumberOfComparisons(int maximumNumberOfComparisons) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withSimilarityMetric(SimilarityMetric similarityMetric) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withMinimumTokenMatch(Integer minimumTokenMatch) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withExclusionFileName(String exclusionFileName) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withSubmissionDirectories(Set submissionDirectories) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withOldSubmissionDirectories(Set oldSubmissionDirectories) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withBaseCodeSubmissionDirectory(File baseCodeSubmissionDirectory) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withSubdirectoryName(String subdirectoryName) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withClusteringOptions(ClusteringOptions clusteringOptions) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withMergingOptions(MergingOptions mergingOptions) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - - public JPlagOptions withNormalize(boolean normalize) { - return new JPlagOptions(language, minimumTokenMatch, submissionDirectories, oldSubmissionDirectories, baseCodeSubmissionDirectory, - subdirectoryName, fileSuffixes, exclusionFileName, similarityMetric, similarityThreshold, maximumNumberOfComparisons, - clusteringOptions, debugParser, mergingOptions, normalize); - } - public boolean hasBaseCode() { return baseCodeSubmissionDirectory != null; } diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java b/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java index e5217330dd..1f4a9a279d 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/BaseCodeReportWriter.java @@ -66,9 +66,11 @@ private BaseCodeMatch convertToBaseCodeMatch(Submission submission, Match match, List tokens = submission.getTokenList().subList(takeLeft ? match.startOfFirst() : match.startOfSecond(), (takeLeft ? match.endOfFirst() : match.endOfSecond()) + 1); - Comparator lineComparator = Comparator.comparingInt(Token::getLine); - Token start = tokens.stream().min(lineComparator).orElseThrow(); - Token end = tokens.stream().max(lineComparator).orElseThrow(); + Comparator lineStartComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); + Comparator lineEndComparator = Comparator.comparingInt(Token::getLine) + .thenComparingInt((Token t) -> t.getColumn() + t.getLength()); + Token start = tokens.stream().min(lineStartComparator).orElseThrow(); + Token end = tokens.stream().max(lineEndComparator).orElseThrow(); CodePosition startPosition = new CodePosition(start.getLine(), start.getColumn() - 1, takeLeft ? match.startOfFirst() : match.startOfSecond()); diff --git a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java index f8732dac44..35ef87de59 100644 --- a/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java +++ b/core/src/main/java/de/jplag/reporting/jsonfactory/ComparisonReportWriter.java @@ -99,12 +99,14 @@ private Match convertMatchToReportMatch(JPlagComparison comparison, de.jplag.Mat List tokensFirst = comparison.firstSubmission().getTokenList().subList(match.startOfFirst(), match.endOfFirst() + 1); List tokensSecond = comparison.secondSubmission().getTokenList().subList(match.startOfSecond(), match.endOfSecond() + 1); - Comparator lineComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); - - Token startOfFirst = tokensFirst.stream().min(lineComparator).orElseThrow(); - Token endOfFirst = tokensFirst.stream().max(lineComparator).orElseThrow(); - Token startOfSecond = tokensSecond.stream().min(lineComparator).orElseThrow(); - Token endOfSecond = tokensSecond.stream().max(lineComparator).orElseThrow(); + Comparator lineStartComparator = Comparator.comparingInt(Token::getLine).thenComparingInt(Token::getColumn); + Comparator lineEndComparator = Comparator.comparingInt(Token::getLine) + .thenComparingInt((Token t) -> t.getColumn() + t.getLength()); + + Token startOfFirst = tokensFirst.stream().min(lineStartComparator).orElseThrow(); + Token endOfFirst = tokensFirst.stream().max(lineEndComparator).orElseThrow(); + Token startOfSecond = tokensSecond.stream().min(lineStartComparator).orElseThrow(); + Token endOfSecond = tokensSecond.stream().max(lineEndComparator).orElseThrow(); String firstFileName = FilePathUtil.getRelativeSubmissionPath(startOfFirst.getFile(), comparison.firstSubmission(), submissionToIdFunction) .toString(); diff --git a/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java b/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java index bb1fb4d399..a1520f92bb 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/ReportObjectFactory.java @@ -149,7 +149,7 @@ private void writeOverview(JPlagResult result) { missingComparisons); OverviewReport overviewReport = new OverviewReport(REPORT_VIEWER_VERSION, folders.stream().map(File::getPath).toList(), // submissionFolderPath baseCodePath, // baseCodeFolderPath - result.getOptions().language().getName(), // language + result.getOptions().language().getIdentifier(), // language result.getOptions().fileSuffixes(), // fileExtensions submissionNameToIdMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)), // submissionIds submissionNameToNameToComparisonFileName, // result.getOptions().getMinimumTokenMatch(), diff --git a/core/src/main/java/de/jplag/reporting/reportobject/model/Version.java b/core/src/main/java/de/jplag/reporting/reportobject/model/Version.java index ddf0fe1a99..7442e310d7 100644 --- a/core/src/main/java/de/jplag/reporting/reportobject/model/Version.java +++ b/core/src/main/java/de/jplag/reporting/reportobject/model/Version.java @@ -1,5 +1,7 @@ package de.jplag.reporting.reportobject.model; +import java.util.Comparator; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,7 +14,8 @@ * @param minor MINOR version when you add functionality in a backwards compatible manner * @param patch PATCH version when you make backwards compatible bug fixes */ -public record Version(@JsonProperty("major") int major, @JsonProperty("minor") int minor, @JsonProperty("patch") int patch) { +public record Version(@JsonProperty("major") int major, @JsonProperty("minor") int minor, @JsonProperty("patch") int patch) + implements Comparable { /** * The default version for development (0.0.0). @@ -42,4 +45,9 @@ public static Version parseVersion(String version) { public String toString() { return String.format("%d.%d.%d", major, minor, patch); } + + @Override + public int compareTo(Version other) { + return Comparator.comparing(Version::major).thenComparing(Version::minor).thenComparing(Version::patch).compare(this, other); + } } diff --git a/core/src/test/java/de/jplag/NewJavaFeaturesTest.java b/core/src/test/java/de/jplag/NewJavaFeaturesTest.java index 6f14ebf068..88b0ecfcba 100644 --- a/core/src/test/java/de/jplag/NewJavaFeaturesTest.java +++ b/core/src/test/java/de/jplag/NewJavaFeaturesTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test; import de.jplag.exceptions.ExitException; -import de.jplag.java.JavaLanguage; public class NewJavaFeaturesTest extends TestBase { @@ -20,17 +19,17 @@ public class NewJavaFeaturesTest extends TestBase { private static final String CHANGE_MESSAGE = "Number of %s changed! If intended, modify the test case!"; private static final String VERSION_MISMATCH_MESSAGE = "Using Java version %s instead of %s may skew the results."; private static final String VERSION_MATCH_MESSAGE = "Java version matches, but results deviate from expected values"; - private static final String JAVA_VERSION_KEY = "java.version"; private static final String CI_VARIABLE = "CI"; + public static final int EXPECTED_JAVA_VERSION = 21; + @Test @DisplayName("test comparison of Java files with modern language features") public void testJavaFeatureDuplicates() throws ExitException { // pre-condition - String actualJavaVersion = System.getProperty(JAVA_VERSION_KEY); + int javaVersion = Runtime.version().feature(); boolean isCiRun = System.getenv(CI_VARIABLE) != null; - boolean isCorrectJavaVersion = actualJavaVersion.startsWith(String.valueOf(JavaLanguage.JAVA_VERSION)); - assumeTrue(isCorrectJavaVersion || isCiRun, VERSION_MISMATCH_MESSAGE.formatted(actualJavaVersion, JavaLanguage.JAVA_VERSION)); + assumeTrue(javaVersion == EXPECTED_JAVA_VERSION || isCiRun, VERSION_MISMATCH_MESSAGE.formatted(javaVersion, EXPECTED_JAVA_VERSION)); JPlagResult result = runJPlagWithExclusionFile(ROOT_DIRECTORY, EXCLUSION_FILE_NAME); diff --git a/core/src/test/java/de/jplag/NormalizationTest.java b/core/src/test/java/de/jplag/NormalizationTest.java index c6a9db9ed1..f1ba200194 100644 --- a/core/src/test/java/de/jplag/NormalizationTest.java +++ b/core/src/test/java/de/jplag/NormalizationTest.java @@ -39,4 +39,4 @@ void testReorderingNormalization() { void testInsertionReorderingNormalization() { Assertions.assertIterableEquals(originalTokenString, tokenStringMap.get("SquaresInsertedReordered.java")); } -} +} \ No newline at end of file diff --git a/core/src/test/java/de/jplag/options/JPlagOptionsTest.java b/core/src/test/java/de/jplag/options/JPlagOptionsTest.java new file mode 100644 index 0000000000..90bb54c153 --- /dev/null +++ b/core/src/test/java/de/jplag/options/JPlagOptionsTest.java @@ -0,0 +1,20 @@ +package de.jplag.options; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import de.jplag.java.JavaLanguage; + +class JPlagOptionsTest { + @Test + void testWithLanguageOption() { + JavaLanguage lang = new JavaLanguage(); + JPlagOptions options = new JPlagOptions(null, Set.of(), Set.of()); + options = options.withLanguageOption(lang); + + assertEquals(lang, options.language()); + } +} diff --git a/core/src/test/java/de/jplag/reporting/jsonfactory/ReportTokenPositionTestTest.java b/core/src/test/java/de/jplag/reporting/jsonfactory/ReportTokenPositionTestTest.java new file mode 100644 index 0000000000..d186863e98 --- /dev/null +++ b/core/src/test/java/de/jplag/reporting/jsonfactory/ReportTokenPositionTestTest.java @@ -0,0 +1,150 @@ +package de.jplag.reporting.jsonfactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import de.jplag.JPlagComparison; +import de.jplag.JPlagResult; +import de.jplag.Match; +import de.jplag.Submission; +import de.jplag.Token; +import de.jplag.options.JPlagOptions; +import de.jplag.reporting.FilePathUtil; +import de.jplag.reporting.reportobject.model.BaseCodeMatch; +import de.jplag.reporting.reportobject.model.CodePosition; +import de.jplag.reporting.reportobject.model.ComparisonReport; + +class ReportTokenPositionTestTest { + + @Test + void testCorrectTokenPositionsInComparisonReport() { + JPlagResult result = mock(JPlagResult.class); + JPlagOptions mockOptions = createMockOptions(); + when(result.getOptions()).thenReturn(mockOptions); + JPlagComparison comparison = mock(JPlagComparison.class); + String firstID = "first"; + String secondID = "second"; + Submission firstSubmission = createMockSubmission(firstID); + Submission secondSubmission = createMockSubmission(secondID); + when(comparison.firstSubmission()).thenReturn(firstSubmission); + when(comparison.secondSubmission()).thenReturn(secondSubmission); + Match mockMatch = createMockMatch(0, 2, 0, 1); + when(comparison.matches()).thenReturn(List.of(mockMatch)); + + when(result.getComparisons(1)).thenReturn(List.of(comparison)); + + TestableReportWriter resultWriter = new TestableReportWriter(); + Map> comparisonReportOutput; + + try (MockedStatic mockedFilePathUtil = Mockito.mockStatic(FilePathUtil.class)) { + mockedFilePathUtil.when(() -> FilePathUtil.getRelativeSubmissionPath(any(), any(), any())).thenReturn(Path.of("file.java")); + comparisonReportOutput = new ComparisonReportWriter(Submission::getName, resultWriter).writeComparisonReports(result); + } + ComparisonReport comparisonReport = (ComparisonReport) resultWriter.getJsonEntry(Path.of(comparisonReportOutput.get(firstID).get(secondID))); + + assertEquals(1, comparisonReport.matches().size()); + + CodePosition startInFirst = comparisonReport.matches().get(0).startInFirst(); + CodePosition endInFirst = comparisonReport.matches().get(0).endInFirst(); + CodePosition startInSecond = comparisonReport.matches().get(0).startInSecond(); + CodePosition endInSecond = comparisonReport.matches().get(0).endInSecond(); + + assertEquals(1, startInFirst.lineNumber()); + assertEquals(0, startInFirst.column()); + assertEquals(0, startInFirst.tokenListIndex()); + + assertEquals(2, endInFirst.lineNumber()); + assertEquals(10, endInFirst.column()); + assertEquals(2, endInFirst.tokenListIndex()); + + assertEquals(1, startInSecond.lineNumber()); + assertEquals(0, startInSecond.column()); + assertEquals(0, startInSecond.tokenListIndex()); + + assertEquals(2, endInSecond.lineNumber()); + assertEquals(10, endInSecond.column()); + assertEquals(1, endInSecond.tokenListIndex()); + } + + @Test + void testCorrectTokenPositionsInBasecodeReport() { + JPlagResult result = mock(JPlagResult.class); + JPlagOptions mockOptions = createMockOptions(); + when(result.getOptions()).thenReturn(mockOptions); + JPlagComparison comparison = mock(JPlagComparison.class); + String submissionID = "first"; + String basecodeID = "basecode"; + Submission submission = createMockSubmission(submissionID); + Submission baseCodeSubmission = createMockSubmission(basecodeID); + when(comparison.firstSubmission()).thenReturn(submission); + when(comparison.secondSubmission()).thenReturn(baseCodeSubmission); + Match mockMatch = createMockMatch(0, 2, 0, 1); + when(comparison.matches()).thenReturn(List.of(mockMatch)); + when(submission.getBaseCodeComparison()).thenReturn(comparison); + + when(result.getComparisons(1)).thenReturn(List.of(comparison)); + + TestableReportWriter resultWriter = new TestableReportWriter(); + try (MockedStatic mockedFilePathUtil = Mockito.mockStatic(FilePathUtil.class)) { + mockedFilePathUtil.when(() -> FilePathUtil.getRelativeSubmissionPath(any(), any(), any())).thenReturn(Path.of("file.java")); + new BaseCodeReportWriter(Submission::getName, resultWriter).writeBaseCodeReport(result); + } + + List baseCodeMatches = (List) resultWriter.getJsonEntry(Path.of("basecode", submissionID + ".json")); + + assertEquals(1, baseCodeMatches.size()); + + CodePosition start = baseCodeMatches.get(0).start(); + CodePosition end = baseCodeMatches.get(0).end(); + + assertEquals(1, start.lineNumber()); + assertEquals(0, start.column()); + assertEquals(0, start.tokenListIndex()); + + assertEquals(2, end.lineNumber()); + assertEquals(10, end.column()); + assertEquals(2, end.tokenListIndex()); + } + + JPlagOptions createMockOptions() { + JPlagOptions options = mock(JPlagOptions.class); + when(options.maximumNumberOfComparisons()).thenReturn(1); + return options; + } + + Submission createMockSubmission(String name) { + Submission submission = mock(Submission.class); + when(submission.getName()).thenReturn(name); + List tokens = List.of(createMockToken(1, 1, 10), createMockToken(2, 1, 10), createMockToken(2, 3, 2), createMockToken(2, 10, 2)); + when(submission.getTokenList()).thenReturn(tokens); + return submission; + } + + Match createMockMatch(int startOfFirst, int endOfFirst, int startOfSecond, int endOfSecond) { + Match match = mock(Match.class); + when(match.startOfFirst()).thenReturn(startOfFirst); + when(match.endOfFirst()).thenReturn(endOfFirst); + when(match.startOfSecond()).thenReturn(startOfSecond); + when(match.endOfSecond()).thenReturn(endOfSecond); + return match; + } + + Token createMockToken(int line, int column, int length) { + Token token = mock(Token.class); + when(token.getLine()).thenReturn(line); + when(token.getColumn()).thenReturn(column); + when(token.getLength()).thenReturn(length); + return token; + } + +} diff --git a/core/src/test/java/de/jplag/reporting/jsonfactory/TestableReportWriter.java b/core/src/test/java/de/jplag/reporting/jsonfactory/TestableReportWriter.java new file mode 100644 index 0000000000..77362106fd --- /dev/null +++ b/core/src/test/java/de/jplag/reporting/jsonfactory/TestableReportWriter.java @@ -0,0 +1,25 @@ +package de.jplag.reporting.jsonfactory; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import de.jplag.reporting.reportobject.writer.DummyResultWriter; + +public class TestableReportWriter extends DummyResultWriter { + + public final Map jsonEntries; + + public TestableReportWriter() { + jsonEntries = new HashMap<>(); + } + + @Override + public void addJsonEntry(Object jsonContent, Path path) { + jsonEntries.put(path, jsonContent); + } + + public Object getJsonEntry(Path path) { + return jsonEntries.get(path); + } +} diff --git a/docs/1.-How-to-Use-JPlag.md b/docs/1.-How-to-Use-JPlag.md index a6e52ff669..c940816d7e 100644 --- a/docs/1.-How-to-Use-JPlag.md +++ b/docs/1.-How-to-Use-JPlag.md @@ -6,8 +6,8 @@ JPlag can be used via the Command Line Interface by executing the JAR file. Example: `java -jar jplag.jar path/to/the/submissions` The language can either be set with the -l parameter or as a subcommand. If both a subcommand and the -l option are specified, the subcommand will take priority. -When using the subcommand language specific arguments can be set. -A list of language specific options can be obtained by requesting the help page of a subcommand (e.g. "jplag java -h"). +When using the subcommand, language-specific arguments can be set. +A list of language-specific options can be obtained by requesting the help page of a subcommand (e.g., "jplag java -h"). The following arguments can be used to control JPlag: ``` @@ -86,7 +86,7 @@ Subcommands (supported languages): *Note that the [legacy CLI](https://github.com/jplag/jplag/blob/legacy/README.md) is varying slightly.* ## Using JPlag programmatically -The new API makes it easy to integrate JPlag's plagiarism detection into external Java projects. +The API makes it easy to integrate JPlag's plagiarism detection into external Java projects. **Example:** @@ -110,16 +110,24 @@ try { ## Report File Generation -After a JPlag run a zipped result report is automatically created. +After a JPlag run, a zipped result report is automatically created. The target location of the report can be specified with the `-r` flag. If the `-r` is not specified, the location defaults `result.zip`. Specifying the `-r` flag with a path `/path/to/desiredFolder` results in the report being created as `/path/to/desiredFolder.zip`. -Unless there is an error during the zipping process, the report will always be zipped. If the zipping process fails, the report will be available as unzipped under the specified location. +The report will always be zipped unless there is an error during the zipping process. If the zipping process fails, the report will be available in an unzipped version under the specified location. ## Viewing Reports -The newest version of the report viewer is always accessible at https://jplag.github.io/JPlag/. Simply drop your `result.zip` folder on the page to start inspecting the results of your JPlag run. Your submissions will neither be uploaded to a server nor stored permanently. They are saved in the application as long as you view them. Once you refresh the page, all information will be erased. +Starting with version v6.0.0, the report viewer is bundled with JPlag and will be launched automatically. The `--mode` option controls this behavior. +By default, JPlag will process the input files and produce a zipped result file. After that, the report viewer is launched (on localhost), and the report will be shown in your browser. + +The option `--mode show` will only open the report viewer. +This allows you to view existing reports. +You can optionally provide the path to a report file to immediately display it in the viewer; otherwise, the viewer will require you to select a report, just like the online version. +By specifying `--mode run`, JPlag will run but generate the zipped report but will not open the report viewer. + +An online version of the viewer is still hosted at https://jplag.github.io/JPlag/ in order to view pre-v6.0.0 reports. Your submissions will neither be uploaded to a server nor stored permanently. They are stored as long as you view them. Once you refresh the page, all information will be erased. ## Basic Concepts @@ -127,7 +135,7 @@ The newest version of the report viewer is always accessible at https://jplag.gi This section explains some fundamental concepts about JPlag that make it easier to understand and use. * **Root directory:** This is the directory in which JPlag will scan for submissions. -* **Submissions:** Submissions contain the source code that JPlag will parse and compare. They have to be direct children of the root directory and can either be single files or directories. +* **Submissions:** Submissions contain the source code JPlag will parse and compare. They have to be direct children of the root directory and can either be single files or directories. ### Single-file submissions @@ -140,7 +148,7 @@ This section explains some fundamental concepts about JPlag that make it easier ### Directory submissions -JPlag will read submission directories recursively, so they can contain multiple (nested) source code files. +JPlag will read submission directories recursively, thus they can contain multiple (nested) source code files. ``` /path/to/root-directory @@ -155,7 +163,7 @@ JPlag will read submission directories recursively, so they can contain multiple └── Utils.java ``` -If you want JPlag to scan only one specific subdirectory of the submissions for source code files (e.g. `src`), can configure that with the argument `-S`: +If you want JPlag to scan only one specific subdirectory of the submissions for source code files (e.g., `src`), you can configure that with the argument `-s`: ``` /path/to/root-directory @@ -173,7 +181,7 @@ If you want JPlag to scan only one specific subdirectory of the submissions for ### Base Code -The base code is a special kind of submission. It is the template that all other submissions are based on. JPlag will ignore all matches between two submissions, where the matches are also part of the base code. Like any other submission, the base code has to be a single file or directory in the root directory. +The base code is a special kind of submission. It is the template that all other submissions are based on. JPlag will ignore all matches between two submissions where the matches are also part of the base code. Like any other submission, the base code has to be a single file or directory in the root directory. ``` /path/to/root-directory @@ -186,7 +194,7 @@ The base code is a special kind of submission. It is the template that all other └── Solution.java ``` -In this example, students have to solve a given problem by implementing the `run` method in the template below. Because they are not supposed to modify the `main` function, it will be identical for each student. +In this example, students must solve a problem by implementing the `run` method in the template below. Because they are not supposed to modify the `main` function, it will be identical for each student. ```java // BaseCode/Solution.java @@ -204,15 +212,15 @@ public class Solution { } ``` -To prevent JPlag from detecting similarities in the `main` function (and other parts of the template), we can instruct JPlag to ignore matches with the given base code by providing the `--bc=` option. +To prevent JPlag from detecting similarities in the `main` function (and other parts of the template), we can instruct JPlag to ignore matches with the given base code by providing the `-bc=` option. The `` in the example above is `BaseCode`. ### Multiple Root Directories -* You can run JPlag with multiple root directories, JPlag compares submissions from all of them +* You can run JPlag with multiple root directories; JPlag compares submissions from all of them * JPlag distinguishes between old and new root directories ** Submissions in new root directories are checked amongst themselves and against submissions from other root directories ** Submissions in old root directories are only checked against submissions from other new root directories -* You need at least one new root directory to run JPlag +* You need at least one (new) root directory to run JPlag This allows you to check submissions against those of previous years: ``` diff --git a/docs/2.-Supported-Languages.md b/docs/2.-Supported-Languages.md index c89bb8ee39..69bbcfc5a0 100644 --- a/docs/2.-Supported-Languages.md +++ b/docs/2.-Supported-Languages.md @@ -1,11 +1,11 @@ -JPlag currently supports Java, C, C++, C#, Go, Kotlin, Python, R, Rust, Scala, Swift, and Scheme. Additionally, it has primitive support for text and prototypical support for EMF metamodels. A detailed list, including the supported language versions can be found in the [project readme](https://github.com/jplag/JPlag/blob/main/README.md#supported-languages). +JPlag currently supports Java, C, C++, C#, Go, Kotlin, Python, R, Rust, Scala, Swift, Javascript, Typescript, LLVM IR and Scheme. Additionally, it has primitive support for text and prototypical support for EMF metamodels. A detailed list, including the supported language versions, can be found in the [project readme](https://github.com/jplag/JPlag/blob/main/README.md#supported-languages). -The language modules differ in their maturity due to their age and different usage frequencies. +The language modules differ in maturity due to their age and differing usage frequencies. Thus, each frontend has a state label: -- `mature`: This module is tried and tested, as well as up to date with a current language version. -- `beta`: This module is relatively new and up to date. However, it is not as well tested. **Feedback welcome!** -- `alpha`: This module is very new and not yet finished. Use with caution! +- `mature`: This module is tried and tested and is (somewhat) up to date with a current language version. +- `beta`: This module is relatively new and (somewhat) up to date. However, it has not been tested as well. **Feedback welcome!** +- `alpha`: This module is very new and has not yet been finished. Use with caution! - `legacy`: This module is from JPlag legacy (pre-v3.0.0) and may only support outdated language versions. It needs an update. -- `unknown`: It is very much unclear in which state this module is. +- `unknown`: It is very unclear which state this module is in. All language modules can be found [here](https://github.com/jplag/JPlag/tree/master/languages). diff --git a/docs/3.-Contributing-to-JPlag.md b/docs/3.-Contributing-to-JPlag.md index dc967f659e..5f770753fd 100644 --- a/docs/3.-Contributing-to-JPlag.md +++ b/docs/3.-Contributing-to-JPlag.md @@ -1,22 +1,39 @@ We're happy to incorporate all improvements to JPlag into this codebase. Feel free to fork the project and send pull requests. If you are new to JPlag, maybe check the [good first issues](https://github.com/jplag/jplag/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). -Please try to make well-documented and clear structured submissions: +Please try to make well-documented and clearly structured submissions: * All artifacts (code, comments...) should be in English * Please avoid abbreviations! -* Make use of JavaDoc to document classes and public methods -* We provide a [formatter configuration](https://github.com/jplag/JPlag/blob/master/formatter.xml), which is enforced by spotless - * Eclipse/IntelliJ users can use it directly - * It can always be applied via maven with `mvn spotless:apply` +* Make use of JavaDoc/TsDoc to document classes and public methods +* We provide a formatter configurations + * For java code we use this [formatter configuration](https://github.com/jplag/JPlag/blob/master/formatter.xml), which is enforced by spotless + * Eclipse/IntelliJ users can use it directly + * It can always be applied via maven with `mvn spotless:apply` + * For typescript/vue code we use prettier und eslint + * They can both be executed with `npm run lint` and `npm run format` + * They can also be executed automatically on commit * Use well-explained pull requests to propose your features -* When re-using code from other projects mark them accordingly and make sure their license allows the re-use +* When re-using code from other projects, mark them accordingly and make sure their license allows the re-use * Your changes should always improve the code quality of the codebase, especially when working on older components -* Your git messages should be concise but more importantly descriptive +* Your git messages should be concise but, more importantly, descriptive * Ensure your git history is clean, or else your PR may get squashed while merging +* When creating a PR, make sure to provide a detailed description of you changes and what purpose they serve ## Building from sources 1. Download or clone the code from this repository. + +### Core 2. Run `mvn clean package` from the root of the repository to compile and build all submodules. - Run `mvn clean package assembly:single` instead if you need the full jar which includes all dependencies. -5. You will find the generated JARs in the subdirectory `jplag.cli/target`. + Run `mvn clean package assembly:single -P with-report-viewer` instead if you need the full jar, which includes all dependencies. +3. You will find the generated JARs in the subdirectory `jplag.cli/target`. + +### Report Viewer +2. Run `npm install` to install all dependencies. +3. Run `npm run dev` to launch the development server. The report viewer will be available at `http://localhost:8080/`. + Different versions of the build command are described in the [report-viewer README](../report-viewer/README.md). + +### Git hooks + +The repository contains a pre-commit hook that prevents commits if they fail spotless and executes prettier and eslint on report-viewer code. +To set up the hooks, call `git config --local core.hooksPath gitHooks/hooks` once within your local repository or run `npm i`/`npm run prepare` in the report-viewer package. diff --git a/docs/4.-Adding-New-Languages.md b/docs/4.-Adding-New-Languages.md index a4dbf71f00..5fd6f876c3 100644 --- a/docs/4.-Adding-New-Languages.md +++ b/docs/4.-Adding-New-Languages.md @@ -1,11 +1,11 @@ -# JPlag Frontend Design +# JPlag Language Module Design -To add support for a new language to JPlag, a JPlag frontend needs to be created for that specific language. The core purpose of a frontend is to transform each submission to a list of _Tokens_, an abstraction of the content of the submission files independent of the language of the submissions.
+To add support for a new language to JPlag, a JPlag language module needs to be created for that specific language. The core purpose of a language module is to transform each submission into a list of _Tokens_, an abstraction of the content of the submission files independent of the language of the submissions.
The token lists of the different submissions are then passed on to a comparison algorithm that checks the token lists for matching sequences. ## How are submissions represented? — Notion of _Token_ -In the context of JPlag, a Token does not represent a lexical unit, as identifiers, keywords or operators. Instead, Tokens represent syntactic entities, like statements, or control structures. More than one token might be needed to represent the nested structure of a statement or expression in a linear token list. +In the context of JPlag, a Token does not represent a lexical unit, as identifiers, keywords or operators. Instead, Tokens represent syntactic entities, like statements or control structures. More than one token might be needed to represent the nested structure of a statement or expression in a linear token list. ```java class MyClass extends SuperClass { private String name; } @@ -18,12 +18,12 @@ Each comment is intended to represent one token. From this example in Java, you may be able to see the following things: - a class declaration is represented by three tokens of different _types_: `CLASS_DECLARATION`, `CLASS_BODY_BEGIN` and `CLASS_BODY_END` - a token is associated with a _position_ in a code file. - - the abstraction is incomplete, many details of the code are omitted. The original code cannot be reconstructed from the token list, but its structure can. + - the abstraction is incomplete, and many details of the code are omitted. The original code cannot be reconstructed from the token list, but its structure can. A few more points about Tokens in JPlag: - a token list contains the Tokens from _all files of one submission_. For that reason, Tokens save the _filename_ of their origin in addition to their position. - Token types are represented by the `TokenType` interface which has to be adapted for each language individually. - - For brevity, each token type is also associated with a String description, usually shorter than their name. Looking at the String representations used in existing frontends, you may recognize a kind of convention about how they are formed. The example above uses the full names of token types. + - For brevity, each token type is also associated with a String description, usually shorter than their name. Looking at the String representations used in existing language modules, you may recognize a kind of convention about how they are formed. The example above uses the full names of token types. ## How does the transformation work? @@ -31,7 +31,7 @@ Here is an outline of the transformation process. - each submitted file is _parsed_. The result is a set of ASTs for each submission. - each AST is now _traversed_ depth-first. The nodes of the AST represent the grammatical units of the language. - upon entering and exiting a node, Tokens can be created that match the type of the node. They are added to the current token list. - - for block-type nodes like bodies of classes or if expressions, the point of entry and exit correspond to the respective `BEGIN` and `END` token types. If done correctly, the token list should contain balanced pairs of matching `BEGIN` and `END` tokens. + - for block-type nodes like bodies of classes or if expressions, the point of entry and exit corresponds to the respective `BEGIN` and `END` token types. If done correctly, the token list should contain balanced pairs of matching `BEGIN` and `END` tokens. ```java @Override @@ -57,20 +57,20 @@ public void enterClassDeclaration(ClassBodyContext context) { addToken(token); } ``` -The way the traversal works and how you can interact with the process depends on the parser technology used. In the example above, **ANTLR-generated parsers** were used, as was in most of the current JPlag frontends. We recommend to use ANTLR for any new frontend. +The way the traversal works and how you can interact with the process depends on the parser technology used. In the example above, **ANTLR-generated parsers** were used, as was in most of the current JPlag language modules. We recommend using ANTLR for any new language module. If a hard-coded (as opposed to dynamically generated) parser library is available for your language, it may make sense to use it. An implementation of the visitor pattern for the resulting AST should be included. -# Frontend Structure +# Language Module Structure -A frontend consists of these parts: +A language module consists of these parts: | Component/Class | Superclass | Function | How to get there | |-----------------------------------------|---------------------------|--------------------------------------------------|-------------------------------------------------------------| -| Language class | `de.jplag.Language` | access point for the frontend | copy with small adjustments | +| Language class | `de.jplag.Language` | access point for the language module | copy with small adjustments | | `pom.xml` | - | Maven submodule descriptor | copy with small adjustments;
add dependencies for parser | -| `README.md` | - | documentation for the frontend | copy for consistent structure; adjust from there | -| TokenType class | `de.jplag.TokenType` | contains the language-specific token types | **implement new** | +| `README.md` | - | documentation for the language module | copy for consistent structure; adjust from there | +| TokenType class | `de.jplag.TokenType` | contains the language-specific token types | **implement new** | | | | | | Lexer and Parser | - | transform code into AST | depends on technology | | ParserAdapter class | `de.jplag.AbstractParser` | sets up Parser and calls Traverser | depends on technology | @@ -84,23 +84,146 @@ For example, if ANTLR is used, the setup is as follows: | Lexer and Parser | `Lexer`, `Parser` (ANTLR) | transform code into AST | generated from grammar files by antlr4-maven-plugin | | Traverser | `ParseTreeWalker` (ANTLR) | traverses AST and calls listener | included in antlr4-runtime library, can be used as is | | TraverserListener class | `ParseTreeListener` (ANTLR) | creates tokens when called | **implement new** | -| ParserAdapter class | `de.jplag.AbstractParser` | sets up Parser and calls Traverser | copy with small adjustments | +| ParserAdapter class | `de.jplag.AbstractAntlrParser` | sets up Parser and calls Traverser | copy with small adjustments | -As the table shows, much of a frontend can be reused, especially when using ANTLR. The only parts left to implement specifically for each frontend are +As the table shows, much of a language module can be reused, especially when using ANTLR. The only parts left to implement specifically for each language module are - the ParserAdapter (for custom parsers) - the TokenTypes, and - the TraverserListener. **Note** for parser libraries other than ANTLR: - It should still be rather easy to implement the ParserAdapter from the library documentation. - - Instead of using a listener pattern, the library may require you to do the token extraction in a _Visitor subclass_. In that case, there is only one method call per element, called e.g. `traverseClassDeclaration`. The advantage of this version is that the traversal of the subtrees can be controlled freely. See the Scala frontend for an example. + - Instead of using a listener pattern, the library may require you to do the token extraction in a _Visitor subclass_. In that case, there is only one method call per element, called e.g. `traverseClassDeclaration`. The advantage of this version is that the traversal of the subtrees can be controlled freely. See the Scala language module for an example. -### Basic procedure outline +## Setting up a new language module with ANTLR + +JPlag provides a small framework to make it easier to implement language modules with ANTLR + +### Create the Language class + +Extends the AbstractAntlrLanguage class and implements all required methods. There are two options for creating the parser. +It can either be passed to the superclass in the constructor, as shown below, or created later by overriding the initializeParser method. +The latter option should be used if the parser requires dynamic parameters. + +```java +public class TestLanguage extends AbstractAntlrLanguage { + public TestLanguage() { + super(new TestParserAdapter()); + } + + @Override + public String[] suffixes() { + return new String[] {"expression"}; //return a list of file suffixes for your language + } + + @Override + public String getName() { + return "Test"; //return the name of the language (e.g. Java). Can be anything that describes the language module shorty + } + + @Override + public String getIdentifier() { + return "test"; //return the identifier for the language (e.g. java). Should be something simple and unique + } + + @Override + public int minimumTokenMatch() { + return 9; //The minimum number of tokens required to form a match. Leave this at 9 if your module doesn't require anything different + } +} +``` + +### Implement the parser adapter + +The generated code by ANTLR always looks slightly different. The AbstractAntlrParserAdapter class is able to perform most of the required steps automatically. +The implementation only needs to call the correct generated methods. They should be named roughly the same as the example. The javadoc of each method contains additional information. + +```java +public class TestParserAdapter extends AbstractAntlrParserAdapter { + private static final TestListener listener = new TestListener(); + + @Override + protected Lexer createLexer(CharStream input) { + return new TestLexer(input); + } + + @Override + protected TestParser createParser(CommonTokenStream tokenStream) { + return new TestParser(tokenStream); + } + + @Override + protected ParserRuleContext getEntryContext(TestParser parser) { + return parser.expressionFile(); + } + + @Override + protected AbstractAntlrListener getListener() { + return listener; + } +} +``` + +### Implement the token type enum + +This is the same as non ANTLR modules. The enum should look something like this: + +```java +public enum TestTokenType implements TokenType { + TOKEN_NAME("TOKEN_DESCRIPTION"); //the description works as a visual name. Look at other language modules for examples + + private final String description; + + TestTokenType(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } +} +``` + +### Implement the listener + +In contrast to the java module, the framework for the ANTLR module a set of extraction rules has to be defined instead of a traditional listener. +All rules are independent of each other, which makes it easier to debug the token extraction. + +The basic structure looks like this: + +```java +class TestListener extends AbstractAntlrListener { + + TestListener() { + //add rules + } +} +``` + +To make the class easier to read the constructor should only call methods which contain the rules. These methods shouldn't be too long and contain the rules for a specific category of token. + +Extraction rules can be very complicated, but in most cases simple ones will suffice. The easiest option is to directly map antlr tokens to JPlag tokens: + +```java +visit(VarDefContext.class).map(VARDEF); +``` + +There are some different variants of map, which determine the length of the tokens. The javadoc contains details on that. Map can also receive two JPlag token types, which creates one JPlag token for the start of the context and one for the end. +visit can also receive a type of ANTLR terminal node to create tokens from terminal nodes. + +Additional features for rules: + +1. Condition - Can be passed as a second argument to visit. The rule only applies if the condition returns true (see CPP language module for examples) +2. Semantics - Can be passed by using withSemantics after the map call (see CPP language module for examples) +3. Delegate - To have more precise control over the token position and length a delegated visitor can be used (see Go language module for examples) + +## Basic procedure outline ```mermaid flowchart LR JPlag -->|"parse(files)"| Language - subgraph frontend[LanguageFrontend] + subgraph module[LanguageModule] Language -->|"parse(files)"| ParserAdapter ParserAdapter -->|"parse(files)"| Parser -.->|ASTs| ParserAdapter ParserAdapter -->|"walk(ASTs)"| Traverser @@ -110,49 +233,37 @@ flowchart LR end ``` -Note: In existing frontends, the token list is managed by the ParserAdapter, and from there it is returned to the +Note: In existing language modules, the token list is managed by the ParserAdapter, and from there it is returned to the Language class and then to JPlag. -### Integration into JPlag +## Integration into JPlag -The following adjustments have to be made beyond creating the frontend submodule itself: +The following adjustments have to be made beyond creating the language module submodule itself: - Register the submodule in the aggregator POM for every build profile. ```xml - + ... - jplag.frontend.my-frontend + newlanguage/module> ... ``` -- Add a dependency from the aggregator module to the new frontend -- Add a dependency from the jplag module to the new frontend -```xml - - - - ... - - de.jplag - jplag.frontend.my-frontend - ${revision} - - ... - -``` +- Add a dependency for the new language module to the coverage-report module +- Add a dependency for the new language module to the cli module +- Update the documentation in the main readme file and the `docs` folder -That's it! The new frontend should now be usable as described in the main README. The name of the frontend used with the CLI `-l` option is the `IDENTIFIER` set in the Language class. +That's it! The new language module should now be usable as described in the main README. The name of the language module used with the CLI `-l` option is the `IDENTIFIER` set in the Language class. # Token Selection -Apart from extracting the tokens correctly, the task of deciding which syntactical elements should be assigned a token is the essential part when designing a frontend.
+Apart from extracting the tokens correctly, the task of deciding which syntactical elements should be assigned a token is an essential part of designing a language module.
This guideline is solely based on experience and intuition – this "worked well" so far. More research might hint towards a more systematic process of token selection. The goal of the abstraction is to create a token list that is - _accurate_: a fair representation of the code as input to the comparison algorithm - _consistent per se_: insensitive to small changes in the code that might obfuscate plagiarism; constructs are represented equally throughout the file - - _consistent_ with the output of other trusted frontends—only to the extent that their respective languages are comparable, naturally. + - _consistent_ with the output of other trusted language modules — only to the extent that their respective languages are comparable, naturally. To create a set of tokens in line with these objectives, we offer the tips below. @@ -222,7 +333,7 @@ Note: If lists of the same type are nested, the end of the inner list may become # Token Extraction -The token extraction is the most time-consuming part of the frontend design. +The token extraction is the most time-consuming part of the language module design. How difficult it is is largely dependent on the underlying **grammar** of the parser. This article deals with the implementation of the listener which is called at every stage of traversal of the AST. The examples center around tokenizing the Java language, using a grammar written in ANTLR4. @@ -424,9 +535,9 @@ options.sensitivity.getValue(); You should pass the options to your parser if neccesary. -# Frontend Test +# Language Module Test -To check the output of your frontend against the input, the `TokenPrinter` can be helpful. The `TokenPrinter` prints the input line by line, and the tokens of each line below it. +To check the output of your language module against the input, the `TokenPrinter` can be helpful. The `TokenPrinter` prints the input line by line, and the tokens of each line below it. ```java 10 public class Example { @@ -447,11 +558,11 @@ To check the output of your frontend against the input, the `TokenPrinter` can b 15 } |}CLASS ``` -To test a frontend, set up a JUnit test class where the `TokenPrinter` prints the output of the `parse` method of the frontend. Read through the output and check whether the `List` satisfies the given requirements. +To test a language module, set up a JUnit test class where the `TokenPrinter` prints the output of the `parse` method of the language module. Read through the output and check whether the `List` satisfies the given requirements. ### Test files -The frontend should be tested with 'authentic' sample code as well as a 'complete' test file that covers all syntactic elements that the frontend should take into account. If you are using an ANTLR parser, such a complete test file may be included in the parser test files in the ANTLRv4 Grammar Repository. +The language module should be tested with 'authentic' sample code as well as a 'complete' test file that covers all syntactic elements that the language module should take into account. If you are using an ANTLR parser, such a complete test file may be included in the parser test files in the ANTLRv4 Grammar Repository. ### Sanity check suggestions @@ -461,7 +572,7 @@ The frontend should be tested with 'authentic' sample code as well as a 'complet - The token list represents the input code with an acceptable coverage —how that can be measured and what coverage is acceptable depends on the language. One approach would be line coverage, e.g. 90 percent of code lines should contain a token. -- There are no `TokenTypes` that can never be produced by the frontend for any input. +- There are no `TokenTypes` that can never be produced by the language module for any input. - Put another way, the complete test code produces a token list that contains every type of token. ### Writing tests using the test api @@ -510,7 +621,32 @@ protected File getTestFileLocation() { } ``` +## Testing token positions + +The precise position of a token can be relevant for the visualization in the report viewer. To make sure the token positions are extracted correctly language modules should include some tests for that. + +Writing such tests can be done using a specific syntax in the test sources directly. +Such a file can look like this: +```java +>class Test { +> int test; +$ | J_VARDEF 8 +>} +``` + +Every line that is prefixed with '>' will be interpreted as a line of test source code. + +Every line starting with '$' contains information about one expected token. The token is expected in the first source line above this one. +The '|' marks the column the token should be in. It is followed by one space, then the name of the token (The name of the enum constant). +Finally, separated by another space is the length of the token. +A single file may contain any number of tokens. +The test will verify that at least one token with those exact properties exists. + +These test files have to be added in the TestDataCollector. Put all test files in a single directory and specify it through collector.addTokenPositionTests(""). +If the directory is in the default location for test files, a relative path is enough, otherwise a full path has to be specified. + + # Adding code highlighting to the report-viewer To ensure your language gets properly registered and its code is correctly highlighted in the report-viewer: -1) Add your language to the `ParserLanguage` enum in 'src/model/Language.ts'. As the value for the entry use its frontend name. -2) Add your language to the switch-case in `src/utils/CodeHighlighter.ts` and return the correct [highlight.js name](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). If your language is not supported by default, also register the language here. \ No newline at end of file +1) Add your language to the `ParserLanguage` enum in 'src/model/Language.ts'. As the value for the entry use its language module name. +2) Add your language to the switch-case in `src/utils/CodeHighlighter.ts` and return the correct [highlight.js name](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). If your language is not supported by default, also register the language here. diff --git a/docs/5.-End-to-End-Testing.md b/docs/5.-End-to-End-Testing.md index 35bfd882c5..db9080e804 100644 --- a/docs/5.-End-to-End-Testing.md +++ b/docs/5.-End-to-End-Testing.md @@ -1,433 +1,8 @@ -## Basics -The basic structure of the end-to-end testing module is discussed in the [corresponding readme file](https://github.com/jplag/JPlag/blob/master/endtoend-testing/README.md). - -## Rationale behind the test data -To be able to create the test data, some examples from science, which have addressed the concealment of plagiarism, were used to ensure the greatest possible coverage of the JPlag functionality. -Here, the changes were split as finely as possible and applied to the levies. -The following elaborations were used for this: -- [Detecting source code plagiarism on introductory programming course assignments using a bytecode approach](https://ieeexplore.ieee.org/abstract/document/7910274) -- [Detecting Disguised Plagiarism](https://arxiv.org/abs/1711.02149) - -These elaborations provide basic ideas on how a modification of the plagiarized source code can look like or be adapted. -These code adaptations refer to a wide range of changes starting from -adding/removing comments to architectural changes in the deliverables. - -- (1) Inserting comments or empty lines -- (2) Changing variable names or function names -- (3) Insertion of unnecessary or changed code lines -- (4) Changing the program flow (statements and functions must be independent from each other) - - (1) Variable declaration at the beginning of the program - - (2) Combining declarations of variables - - (3) Reuse of the same variable for other functions -- (5) Changing control structures - - (1) for(...) to while(...) - - (2) if(...) to switch-case -- (6) Modification of expressions - - (1) (X < Y) to !(X >= Y) and ++x to x = x + 1 -- (7) Splitting and merging statements - - (1) x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z; -- (8) Inserting unnecessary casts - -These changes were now transferred to a base class and thus the plagiarism was created. The named base class was provided with the individual changes. The numbers in the list shown above are intended for the traceability of the test data. Here the test data filenames were named with the respective changes. Example: SortAlgo4d1 contains the changes "Variable declaration at the beginning of the program". If several points are combined, this is separated by "_" e.g.: SortAlgo1_3 contains "(1) Inserting comments or empty lines" and "(3) Insertion of unnecessary or changed code lines". - -The following code examples show how these changes affect the program code and also how the detection of JPLag behaves. -All the code examples shown and more can be found at [testdata-resources-SortAlgo](https://github.com/jplag/JPlag/tree/main/endtoend-testing/src/test/resources/data/sortAlgo). - -### (1) Inserting comments or empty lines - -Adding empty lines or comments affects the normalization of the output. If the End-To-End tests fail with these changes, it means that something has changed in the normalization, e.g. removing empty lines or recognizing comments no longer works. - -In the following, the modified base class looks like this: - -Original: -``` java - - public void BubbleSortWithoutRecursion(Integer arr[]) { - for(int i = arr.length; i > 1 ; i--) { -``` - -Plagiarized: -``` java -/* - - Unnecessary comment - */ - public void BubbleSortWithoutRecursion(Integer arr[]) { - //Unnecessary comment - for(int i = arr.length; i > 1 ; i--) { -``` - -As expected, the resulting outputs have a match of 100% (JPLag result): - -``` json -"SortAlgo-SortAlgo1" : { - "minimal_similarity" : 100.0, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -### (2) Changing variable names or function names - -Changing variable names and function names has, like point 1, also the goal of detecting adjustments in the normalization level. -If the End-To-End tests fail with these changes, it means that something has changed in the normalization, e.g. creating constants function and variable names. - -Orginal: - -``` java - private final void swap(T[] arr, int i, int j) { - T t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; - } -``` - -Plagiarized: - -``` java - private final void paws(T[] otherArr, int i, int j) { - T t = otherArr[i]; - otherArr[i] = otherArr[j]; - otherArr[j] = t; - } -``` - -As expected, the resulting outputs have a match of 100% (JPLag result): - -``` json -"SortAlgo-SortAlgo2" : { - "minimal_similarity" : 100.0, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -### (3) Insertion of unnecessary or changed code lines - -In contrast to points 1 and 2, adding unnecessary code lines reduces the token generation. This has the consequence that the recognition can no longer be 100% sure whether plagiarism is present or not. The failure of the end-to-end tests in these cases means that either the tokens have been adjusted, the normalization has changed the function separation or something has changed in the minimum token numbers. This can be easily seen by running the end-to-end tests in different options. this will be shown in the next result examples. - -Original: -``` java - private final void swap(T[] arr, int i, int j) { - T t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; - } -``` -Plagiarized: -``` java -private final void swap(T[] arr, int i, int j) { - var tempVar1 = 0; - if (true) { - T t = arr[i]; - arr[i] = arr[j]; - arr[j] = t; - var tempVar2 = 0; - tempVar2++; - tempVar2 = tempVar2 + 1; - } - } -``` - -The results for the recognition already allow first recognition changes. Here the change of the `minimum_token_match` also has an effect on the result, which was not the case with (1) and (2). - -``` json -[{"options" : { - "minimum_token_match" : 1 - }, -"SortAlgo-SortAlgo3" : { - "minimal_similarity" : 81.159424, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -}] -``` - -``` json -[{"options" : { - "minimum_token_match" : 15 - }, -"SortAlgo-SortAlgo3" : { - "minimal_similarity" : 57.971016, - "maximum_similarity" : 71.42857, - "matched_token_number" : 40 - }, -}] -``` - -### (4) Changing the program flow (statements and functions must be independent from each other) - -This subitem breaks down into three more change methods to maintain fine granularity: -- (1) Variable declaration at the beginning of the program -```java -public class SortAlgo4d1 { - private int firstCounter; - private int arrayLenght; - private int swapVarI; - private int swapVarJ; - -``` - -- (2) Combining declarations of variables -``` java -public class SortAlgo4d2 { - private int firstCounter,swapVarJ,arrayLenght ,swapVarI; -``` - -- (3) Reuse of the same variable for other functions -``` java -public class SortAlgo4d3 { - private int firstCounterAndArrayLenghtAndswapVarJ ,swapVarI; -``` - -The adjustments to the program flow with the previous instantiation of the variables were also made: -Original: -``` java - if (n == 1) - { - return; - } -``` - -Plagiarized: -``` java - firstCounter = n; - if (firstCounter == 1) { - return; - } -``` - -The results of the individual adjustment are as follows: -```json - "SortAlgo-SortAlgo4d1" : { - "minimal_similarity" : 87.30159, - "maximum_similarity" : 98.21429, - "matched_token_number" : 55 - }, - "SortAlgo-SortAlgo4d2" : { - "minimal_similarity" : 87.5, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, - "SortAlgo-SortAlgo4d3" : { - "minimal_similarity" : 90.32258, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -### (5) Changing control structures - -The change of the control structure in the program also indicates a change of the token generation in case of faulty tests. In contrast to (4), however, these are specially designed for other tokens that are made for if, else, ... structures. - -These changes were made to the SortAlgo test data in a plagiarized form. - -Original: -``` java - public void BubbleSortRecursion(Integer arr[], int n) { - if (n == 1) - { - return; - } - - for (int i = 0; i < n - 1; i++) - { - if (arr[i] > arr[i + 1]) - { - swap(arr, i , i+1); - } - } - BubbleSortRecursion(arr, n - 1); - } -``` - -Plagiarized: -``` java - public void BubbleSortRecursion(Integer arr[], int n) { - switch (n) { - case 1: - return; - } - - int i = 0; - while(i < n-1) - { - var tempBool = arr[i] > arr[i + 1]; - if (tempBool) { - swap(arr, i, i + 1); - } - i++; - } - - BubbleSortRecursion(arr, n - 1); - } -``` - -Here it is remarkable which affects the adjustment of the `minimum_token_match` has on the recognition of the plagiarism. -Changes of the token generation as well as the `minimum_token_match` have an effect on this kind of End-To-End test. - -``` json - "options" : { - "minimum_token_match" : 1 - }, - "tests" : { - "SortAlgo-SortAlgo5" : { - "minimal_similarity" : 82.14286, - "maximum_similarity" : 82.14286, - "matched_token_number" : 46 - }, -``` - -``` json - "options" : { - "minimum_token_match" : 15 - }, - "tests" : { - "SortAlgo-SortAlgo5" : { - "minimal_similarity" : 0.0, - "maximum_similarity" : 0.0, - "matched_token_number" : 0 - }, -``` - -### (6) Modification of expressions -Changing the order of compare also changes the order of the program flow which is difficult to determine the exact effect of plagiarism. Here the statements (X < Y) to !(X >= Y) and ++x to x = x + 1 are changed. Since the syntax should be recognized however as expression, the pure change of the expression has little effect on their plagiarism recognition. - -Orginal: -``` java - public void BubbleSortRecursion(Integer arr[], int n) { - if (n == 1) - { - return; - } - - for (int i = 0; i < n - 1; i++) - { - if (arr[i] > arr[i + 1]) - { - swap(arr, i , i+1); - } - } - BubbleSortRecursion(arr, n - 1); - } -``` - -Plagiarized: -``` java -public void BubbleSortRecursion(Integer arr[], int n) { - if (n != 1) - { - for (int i = 0; !(i >= (n - 1));) - { - if (!(arr[i] <= arr[i + 1])) - { - swap(arr, i , i+1); - } - i = i + 1; - } - BubbleSortRecursion(arr, n - 1); - } - else - { - return; - } - } -``` - -Results: -``` json - { - "options" : { - "minimum_token_match" : 1 - }, - "SortAlgo-SortAlgo6" : { - "minimal_similarity" : 83.58209, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -``` json - "options" : { - "minimum_token_match" : 15 - }, - "SortAlgo-SortAlgo6" : { - "minimal_similarity" : 43.28358, - "maximum_similarity" : 51.785713, - "matched_token_number" : 29 - }, -``` - -### (7) Splitting and merging statements -The merging or splitting of statements results in changing the token for the respective plagiarism detection. -Here code lines are either fetched from functions or stored in functions like `x = getSomeValue(); y = x- z;` to `y = (getSomeValue() - Z`. - -Original: -``` java -[...] - swap(arr, i , i+1); -[...] - if (arr[innerCounter] > arr[innerCounter + 1]) { -[...] -``` - -Plagiarized: -``` java -[...] - swap(arr, i, add(i , 1)); -[...] - if (arr[innerCounter] > arr[add(innerCounter , 1)]) { -[...] - private int add(int value1, int value2) - { - return value1 + value2; - } - - private int subtract(int value1, int value2) - { - return value1 - value2; - } -``` - -Results: -``` json - "options" : { - "minimum_token_match" : 1 - }, - "tests" : { - "SortAlgo-SortAlgo7" : { - "minimal_similarity" : 76.712326, - "maximum_similarity" : 100.0, - "matched_token_number" : 56 - }, -``` - -``` json - "options" : { - "minimum_token_match" : 15 - }, - "tests" : { - "SortAlgo-SortAlgo7" : { - "minimal_similarity" : 49.315067, - "maximum_similarity" : 64.28571, - "matched_token_number" : 36 - }, -``` - -## Summary - -The results and the test coverage of the end-to-end tests strongly depend on the tested plagiarisms. It is also important to use and test many different options of the API offered by JPlag, as these have a direct influence on the detection and are therefore also important for the change detection. - -To summarize -- (1) and (2) test normalization level -- (3) to (7) the token generation level - -If a result differs only in the options, it is very likely that the change is in the configuration of the `minimum_token_match`. -This means that if Option1 does not change in the result of the detection, but the result in Option2 does, this is the basis of the `minimum_token_match`. -``` -java: (1)SortAlgo-SortAlgo5 --> passed -java: (15)SortAlgo-SortAlgo5 --> failed -``` - +JPlag has two different types of end-to-end tests. +# Maven end-to-end-tests +There is a module inside the maven project, that runs e2e tests on the core of Jplag. +The basic structure of the end-to-end testing module is discussed in the [corresponding readme file](https://github.com/jplag/JPlag/blob/master/endtoend-testing/README.md). +# Complete end-to-end-tests +The complete end-to-end tests are executed by the [complete e2e tests workflow](../../../.github/workflows/complete-e2e.yml) and are meant to check the entire process from building and executing JPlag to viewing the report in the report viewer. Details are specified in the [corresponding readme file](../report-viewer/tests/e2e/README.md). \ No newline at end of file diff --git a/docs/6.-Report-File-Generation.md b/docs/6.-Report-File-Generation.md index 406ce95cc9..27f8178c7c 100644 --- a/docs/6.-Report-File-Generation.md +++ b/docs/6.-Report-File-Generation.md @@ -32,6 +32,11 @@ result.zip │ submission2-submission....json │ submission2-submissionN.json │ ... +│ +└───basecode +│ └───submissionId1.json +│ └───submissionId2.json +│ ... ``` The report zip contains @@ -41,20 +46,21 @@ The report zip contains - The `overview.json` encapsulates the main information from a JPlagResult such as base directory path, language, min- and max-metric, etc. The `overview.json` provides data to the `OverviewView.vue` that is first displayed after the report is dropped into the viewer. Corresponds to the Java record `OverviewReport`. - submissionFileIndex.json - - The `submissionFileIndex.json` stores a list of all files in the submission for each submission id. + - The `submissionFileIndex.json` stores a list of all files in the submission for each submission id. This file is also used to track the tokens per file. It is represented by a Map from the submission id to an instance of `SubmissionFile`. - options.json - - This File contains all options given to JPlag either over the CLI or programmatically + - This File contains all options given to JPlag either over the CLI or programmatically. It is represented directly by the `JPlagOptions` class. - submissions - - This folder contains all files of all submissions JPlag was run with. For each submission the `submissions` folder contains a subfolder with the name of the corresponding submission id. A subfolder for a submission contains all files of said submission. These files are displayed in the `ComparisonView.vue` - comparison files - - For each submission pair submission1 submission2 with ids submissionId1 and submissionId2, the report contains either submissionId1-submissionId2.json or submissionId2-submissionId1.json. This file contains information the comparison between the two submissions, such as the similarity and concrete matches. Corresponds to the Java record `ComparisonReport`. +- base code + - Each JSON file in the `basecode` folder contains the data where the provided basecode was found in each submission. Each submission has its own file. If no basecode was provided, each file contains an empty array of matches. Each JSON file corresponds to an array of the Java record `BaseCodeMatch`. + ## Submission ids ### Report Viewer @@ -62,7 +68,7 @@ The `overview.json` contains a map that associates a submission id to its displa For internal use in the report viewer use only(!) the submission id. Whenever the name of a submission has to be displayed in the report viewer, the id has to be resolved to its display name first. The report viewer's vuex store provides a getter for this resolution. ### JPlag -At the beginning of report generation a map and a function that associate a JPlag `Submission` to a submission id is built. Whenever you reference a submission in a report viewer DTO use this map/function to resolve the submission to its id. +At the beginning of report generation a map and a function that associates a JPlag `Submission` to a submission id is built. Whenever you reference a submission in a report viewer DTO use this map/function to resolve the submission to its id. ## Adding and displaying new attributes from JPlagResult @@ -89,5 +95,5 @@ Task: Adding the number of tokens in a match, which has to be displayed in the M 2. Modify the existing component `ComparisonReportWriter.java` to additionally extract the number of tokens in a match from the `JPlagResult.java` and save it in the Match DTO 3. Add `tokens: number` to `Match.ts` -4. Edit `ComparisonFactory.ts` to get the number of tokens from the JSON report file. [report-viewer] -5. Edit `MatchTable.vue` to display the tokens number in the `ComparisonView.vue`. +4. Edit `ComparisonFactory.ts` to get the number of tokens from the JSON report file. +5. Edit `MatchList.vue` to display the tokens number in the `ComparisonView.vue`. \ No newline at end of file diff --git a/docs/7.-Clustering-of-Submissions.md b/docs/7.-Clustering-of-Submissions.md index be1aee66a5..0875cf0dc6 100644 --- a/docs/7.-Clustering-of-Submissions.md +++ b/docs/7.-Clustering-of-Submissions.md @@ -1,6 +1,6 @@ ## Clustering Usage -By default, JPlag is configured to perform a clustering of the submissions. +By default, JPlag is configured to cluster the submissions. The clustering partitions the set of submissions into groups of similar submissions. The found clusters can be used candidates for _potentially_ colluding groups. Each cluster has a strength score, that measures how _suspicious_ the cluster is compared to other clusters. @@ -20,18 +20,18 @@ __The clustering is designed to work out-of-the-box for running within the magni | General | Enable | Controls whether the clustering is run at all. | `true` | | General | Algorithm | Which clustering algorithm to use.
Agglomerative Clustering
Agglomerative Clustering iteratively merges similar submissions bottom up. It usually requires manual tuning for its parameters to yield helpful clusters.
Spectral Clustering
Spectral Clustering is combined with Bayesian Optimization to execute the k-Means clustering algorithm multiple times, hopefully finding a \"good\" clustering automatically. Its default parameters should work O.K. in most cases.
| Agglomerative Clustering | | General | Metric | The similarity score between submissions to use during clustering. Each score is expressed in terms of the size of the submissions `A` and `B` and the size of their matched intersection `A ∩ B`.
AVG (aka. Dice's coefficient)
`AVG = 2 * (A ∩ B) / (A + B)`
MAX (aka. overlap coefficient)
`MAX = (A ∩ B) / min(A, B)` Compared to MAX, this prevents obfuscation when a collaborator bloats his submission with unrelated code.
MIN (_deprecated_)
`MIN = (A ∩ B) / max(A, B)`
INTERSECTION (_experimental_)
`INTERSECTION = A ∩ B`
| AVG | -| Spectral | Bandwidth | For Spectral Clustering, Baysian Optimization is used to determine a fitting number of clusters. If a good clustering result is found during the search, numbers of clusters that differ by something in range of the bandwidth are also expected to good. Low values result in more exploration of the search space, high values in more exploitation of known results. | 20.0 | +| Spectral | Bandwidth | For Spectral Clustering, Bayesian Optimization determines a fitting number of clusters. If a good clustering result is found during the search, numbers of clusters that differ by something in range of the bandwidth are also expected to good. Low values result in more search space exploration, and high values result in more exploitation of known results. | 20.0 | | Spectral | Noise | The result of each k-Means run in the search for good clusterings is random. The noise level models the variance in the \"worth\" of these results. It also acts as a regularization constant. | 0.0025 | -| Spectral | Min-Runs | Minimum number of k-Means executions for spectral clustering. With these initial runs clustering sizes are explored. | 5 | -| Spectral | Max-Runs | Maximum number of k-Means executions during spectral clustering. Any execution after the initial (min-) runs tries to balance between exploration of unknown clustering sizes and exploitation of clustering sizes known as good. | 50 | +| Spectral | Min-Runs | Minimum number of k-Means executions for spectral clustering. With these initial runs, clustering sizes are explored. | 5 | +| Spectral | Max-Runs | Maximum number of k-Means executions during spectral clustering. Any execution after the initial (min-) runs tries to balance between exploring unknown clustering sizes and exploiting clustering sizes known as good. | 50 | | Spectral | K-Means Iterations | Maximum number of iterations during each execution of the k-Means algorithm. | 200 | -| Agglomerative | Threshold | Only clusters with an inter-cluster-similarity greater than this threshold are merged during agglomerative clustering. | 0.2 | +| Agglomerative | Threshold | Only clusters with an inter-cluster similarity greater than this threshold are merged during agglomerative clustering. | 0.2 | | Agglomerative | inter-cluster-similarity | How to measure the similarity of two clusters during agglomerative clustering.
MIN (aka. complete-linkage)
Clusters are merged if all their submissions are similar.
MAX (aka. single-linkage)
Clusters are merged if there is a similar submission in both.
AVERAGE (aka. average-linkage)
Clusters are merged if their submissions are similar on average.
| AVERAGE | -| Preprocessing | Pre-Processor | How the similarities are preprocessed prior to clustering. Spectral Clustering will probably not have good results without it.
None
No preprocessing.
Cumulative Distribution Function (CDF)
Before clustering, the value of the cumulative distribution function of all similarities is estimated. The similarities are multiplied with these estimates. This has the effect of suppressing similarities that are low compared to other similarities.
Percentile
Any similarity smaller than the given percentile will be suppressed during clustering.
Threshold
Any similarity smaller than the given threshold will be suppressed during clustering.
| CDF | +| Preprocessing | Pre-Processor | How the similarities are preprocessed before clustering. Spectral Clustering will probably not have good results without it.
None
No preprocessing.
Cumulative Distribution Function (CDF)
Before clustering, the value of the cumulative distribution function of all similarities is estimated. The similarities are multiplied by these estimates. This has the effect of suppressing low similarities compared to other similarities.
Percentile
Any similarity smaller than the given percentile will be suppressed during clustering.
Threshold
Any similarity smaller than the given threshold will be suppressed during clustering.
| CDF | ## Clustering Architecture -All clustering related classes are contained within the `de.jplag.clustering(.*)` packages in the core project. +All clustering-related classes are contained within the core project's `de.jplag.clustering(.*)` packages. The central idea behind the structure of clustering is the ease of use: To use the clustering calling code should only ever interact with the `ClusteringOptions`, `ClusteringFactory`, and `ClusteringResult` classes: @@ -85,7 +85,7 @@ New clustering algorithms and preprocessors can be implemented using the `Generi ### Integration Tests -There are integration tests for the Spectral Clustering to verify, that a least in the case of two known sets of similarities the groups known to be colluders are found. However, these are considered to be sensitive data. The datasets are not available to the public and these tests can only be run by maintainers with access. +There are integration tests for the Spectral Clustering to verify that, at least in the case of two known sets of similarities, the groups known to be colluders are found. However, these are considered to be sensitive data. The datasets are not available to the public and these tests can only be run by maintainers with access. To run these tests the contents of the [PseudonymizedReports](https://github.com/jplag/PseudonymizedReports) repository must added in the folder `jplag/src/test/resources/de/jplag/PseudonymizedReports`. diff --git a/docs/Home.md b/docs/Home.md index d6388bd543..94cedf59c1 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -3,9 +3,9 @@

## What is JPlag -JPlag finds pairwise similarities among a set of multiple programs. It can reliably detect software plagiarism and collusion in software development. All similarities are calculated locally, and no source code or plagiarism results are ever uploaded to the internet. JPlag supports a large number of programming and modeling languages. JPlag does not merely compare bytes of text but is aware of programming language syntax and program structure and hence is robust against many kinds of attempts to disguise similarities (_obfusction_) between plagiarized files. +JPlag finds pairwise similarities among a set of multiple programs. It can reliably detect software plagiarism and collusion in software development. All similarities are calculated locally; no source code or plagiarism results are ever uploaded online. JPlag supports a large number of programming and modeling languages. JPlag does not merely compare bytes of text but is aware of programming language syntax and program structure and hence is robust against many kinds of attempts to disguise similarities (_obfusction_) between plagiarized files. -JPlag is typically used to detect and thus discourage the unallowed copying of student exercise programs in programming education. But in principle, it can also be used to detect stolen software parts among large amounts of source text or modules that have been duplicated (and only slightly modified). JPlag has already played a part in several intellectual property cases where it has been successfully used by expert witnesses. +JPlag is typically used to detect and thus discourage the unallowed copying of student exercise programs in programming education. However, in principle, it can also detect stolen software parts among large amounts of source text or modules that have been duplicated (and only slightly modified). JPlag has already played a part in several intellectual property cases where expert witnesses have successfully used it. **Just to make it clear**: JPlag does not compare to the internet! It is designed to find similarities among the student solutions, which is usually sufficient for computer programs. @@ -16,7 +16,7 @@ JPlag is typically used to detect and thus discourage the unallowed copying of s * 🤩 [Give us Feedback in a **short (<5 min) survey**](https://docs.google.com/forms/d/e/1FAIpQLSckqUlXhIlJ-H2jtu2VmGf_mJt4hcnHXaDlwhpUL3XG1I8UYw/viewform?usp=sf_link) ## History -Originally, JPlag was developed in 1996 by Guido Mahlpohl and others at the chair of Prof. Walter Tichy at Karlsruhe Institute of Technology (KIT). It was first documented in a [Tech Report](https://publikationen.bibliothek.kit.edu/542000) in 2000 and later more formally in the [Journal of Universal Computer Science](http://www.ipd.kit.edu/tichy/uploads/publikationen/16/finding_plagiarisms_among_a_set_of_progr_638847.pdf). Since 2015 JPlag is hosted here on GitHub. After 25 years of its creation, JPlag is still used frequently in many universities in different countries around the world. +Originally, JPlag was developed in 1996 by Guido Mahlpohl and others at the chair of Prof. Walter Tichy at Karlsruhe Institute of Technology (KIT). It was first documented in a [Tech Report](https://publikationen.bibliothek.kit.edu/542000) in 2000 and later more formally in the [Journal of Universal Computer Science](http://www.ipd.kit.edu/tichy/uploads/publikationen/16/finding_plagiarisms_among_a_set_of_progr_638847.pdf). Since 2015 JPlag is hosted here on GitHub. After 25 years of its creation, JPlag is still used frequently in many universities in different countries worldwide. ## Download JPlag Download the latest version of JPlag [here](https://github.com/jplag/jplag/releases). If you encounter bugs or other issues, please report them [here](https://github.com/jplag/jplag/issues). diff --git a/endtoend-testing/README.md b/endtoend-testing/README.md index 4a82070bdf..f27e7019d8 100644 --- a/endtoend-testing/README.md +++ b/endtoend-testing/README.md @@ -1,114 +1,73 @@ - # JPlag - End-To-End Testing -With the help of the end-to-end module, changes to the detection of JPlag are to be tested. -With the help of elaborated plagiarism, which has been worked out from suggestions in the literature on the topic of "plagiarism detection and avoidance", a wide range of detectable changes can be covered. The selected plagiarisms are the decisive factor here as to whether a change in recognition can be perceived. -## References -These elaborations provide basic ideas on how a modification of the plagiarized source code can look or be adapted. -These code adaptations refer to various changes, from -adding/removing comments to architectural changes in the deliverables. +The end-to-end test module contains tests that report any chance in the similarities reported by JPlag. +There are two kinds of tests: +1. Simple tests that fail if the similarity between two submissions changed +2. Gold standard tests -The following elaborations were used to be able to create the plagiarisms with the broadest coverage: -- [Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim](https://ieeexplore.ieee.org/abstract/document/7910274 "Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim") -- [Detecting Disguised Plagiarism - Hatem A. Mahmoud](https://arxiv.org/abs/1711.02149 "Detecting Disguised Plagiarism - Hatem A. Mahmoud") -- [Instructor-centric source code plagiarism detection and plagiarism corpus](https://dl.acm.org/doi/abs/10.1145/2325296.2325328 "Instructor-centric source code plagiarism detection and plagiarism corpus") +## Gold standard tests -## Steps Towards Plagiarism -The following changes were applied to sample tasks to create test cases: -
    -
  • Inserting comments or empty lines (normalization level)
  • -
  • Changing variable names or function names (normalization level)
  • -
  • Insertion of unnecessary or changed code lines (token generation)
  • -
  • Changing the program flow (token generation) (statements and functions must be independent of each other)
  • -
      -
    • Variable declaration at the beginning of the program
    • -
    • Combining declarations of variables
    • -
    • Reuse of the same variable for other functions
    • -
    -
  • Changing control structures
  • -
      -
    • for(...) to while(...)
    • -
    • if(...) to switch-case
    • -
    -
  • Modification of expressions
  • -
      -
    • (X < Y) to !(X >= Y) and ++x to x = x + 1
    • -
    -
  • Splitting and merging statements
  • -
      -
    • x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z;
    • -
    -
+A gold standard test serves as a metric for the change in detection quality. It needs a list of plagiarism instances in the data set. +JPlag outputs comparisons split into those that should be reported as plagiarism and those that shouldn't. +The test will fail if the average similarity on one of those groups changed. In contrast to the other kind of test, this offers a rough way to check if the changes made JPlag better or worse. -More detailed information about the creation as well as about the subject of the issue can be found in the issue [Develop an end-to-end testing strategy](https://github.com/jplag/JPlag/issues/193 "Develop an end-to-end testing strategy"). +## Updating tests -**The changes listed above have been developed and evaluated for purely scientific purposes and are not intended to be used for plagiarism in the public or private domain.** +If the similarities reported by JPlag change and these changes are wanted, the reference values for the end-to-end tests need to be updated. +To do that the test in [EndToEndGeneratorTest.java](src/test/java/de/jplag/endtoend/EndToEndGeneratorTest.java) have to be executed. +This will generate new reference files. -## JPlag - End-To-End TestSuite Structure -The construction of an end-to-end test is done with the help of the JPlag API. -The tests are generated dynamically according to the existing test data and allow the creation of end-to-end tests for all supported languages of JPlag without making any changes to the code. -The helper loads the existing test data from the designated directory and creates dynamic tests for the individual directories. It is possible to create different test classes for the other languages. - -To distinguish which domain of the recognition changes have occurred, fine granular test cases are used. These are composed of the changes already mentioned above. The plagiarism is compared with the original delivery; thus, detecting and testing small sections of the recognition is possible. - -The comparative values were discussed and tested. The following results of the JPlag scan are used for the comparison: -1. minimal similarity as `double` -2. maximum similarity as `double` -3. matched token number as `int` - -The comparative values were discussed and elaborated in the issue [End-to-end testing - "comparative values"](https://github.com/jplag/JPlag/issues/548 "End-to-end testing - \"comparative values\""). - -Additionally, it is possible to create several options for the test data. More information about the test options can be found at [JPlag - option variants for the end-to-end tests #590](https://github.com/jplag/JPlag/issues/590 "JPlag - option variants for the end-to-end tests #590"). Currently, various settings are supported by the `minimumTokenMatch`. This can be extended as desired in the record class `Options`. - -The current JPlag scans will be compared with the stored ones. -This was done by storing the data in a *.json file which is read at the beginning of each test run. - -### JSON Result Structure - -The structures of the JSON file can be traced using the individual record classes, which can be found under `de.jplag.endtoend.model`. -The outer structure of the JSON file is recorded in the `ResultDescription` record. -The record contains a map of several options and the corresponding results. -The internal structure consists of several `Option` records, each containing information about the test run's current configuration. -Thus the results can be kept apart from the other configurations. -The test results for the specified options are also specified in the object. This consists of the `ExpectedResult` record, which contains the detection results. - -Here the hierarchy is as follows: - -```JSON -[{ - "options":{ - "minimum_token_match":"int" - }, - "tests":{ - "languageIdentifier":{ - "minimal_similarity":"double", - "maximum_similarity":"double", - "matched_token_number":"int" - }, - "/..." - } -}, -{ - "options":{ - "minimum_token_match":"int" - }, - "tests":{ - "languageIdentifier":{ - "minimal_similarity":"double", - "maximum_similarity":"double", - "matched_token_number":"int" - }, - "/..." - } -}] -``` +## Adding new tests + +This segment explains the steps for adding new test data + +### Obtain test data + +New test data can be obtained in multiple ways. + +Ideally, real-world data is used. To use gold standard tests, real-world data needs to contain information about which submission pairs are plagiarism and which aren't. + +Alternatively, test data can be generated using various methods. One such method is explained below. ---- +The test data should be placed under [data](src/test/resources/data). It can either be added as a directory containing submissions or as a zip file. -## Create New Language End-To-End Tests +### Defining the data set for the tests + +This is done in [dataSets](src/test/resources/dataSets). To add a new data set a new json file needs to be placed here. + +A minimum example for a configuration can be found in [progpedia.json](src/test/resources/dataSets/progpedia.json). A full example using all options in [sortAlgo.json](src/test/resources/dataSets/sortAlgo.json). + +For available options look at [dataSetTemplate.json](src/test/resources/dataSetTemplate.json). + +### Generating the reference results + +See Updating tests above + +## Creating test data manually + +The following changes were applied to sample tasks to create the sortAlgo data set: + +* Inserting comments or empty lines (normalization level) +* Changing variable names or function names (normalization level) +* Insertion of unnecessary or changed code lines (token generation) +* Changing the program flow (token generation) (statements and functions must be independent of each other) + * Variable declaration at the beginning of the program + * Combining declarations of variables + * Reuse of the same variable for other functions +* Changing control structures + * for(...) to while(...) + * if(...) to switch-case +* Modification of expressions + * (X < Y) to !(X >= Y) and ++x to x = x + 1 +* Splitting and merging statements + * x = getSomeValue(); y = x- z; to y = (getSomeValue() - Z; + +More detailed information about the creation as well as about the subject of the issue can be found in the issue [Develop an end-to-end testing strategy](https://github.com/jplag/JPlag/issues/193 "Develop an end-to-end testing strategy"). + +**The changes listed above have been developed and evaluated for purely scientific purposes and are not intended to be used for plagiarism in the public or private domain.** -This section explains how to create new end-to-end tests in the existing test suite. ### Creating The Plagiarism + Before you add a new language to the end-to-end tests, I would like to point out that the quality of the tests depends dreadfully on the plagiarism techniques you choose, which were explained in section [Steps Towards Plagiarism](#steps-towards-plagiarism). If you need more information about creating plans for this purpose, you can also read the elaborations that can be found under [References](#references). The more various changes you apply, the more accurate the end-to-end tests for the language will be. @@ -157,69 +116,13 @@ public void BubbleSortWithoutRecursion(Integer arr[]) { //... } ``` -### Copying Plagiarism To The Resources - -The plagiarisms created in [Creating The Plagiarism](#creating-the-plagiarism) must now be copied to the corresponding resources folder. For each test suite, the resources must be placed in `JPlag/jplag.endToEndTesting/src/test/resources/languageTestFiles//`. For example, for the existing test suite `sortAlgo` of language `java`, the path is `JPlag/jplag.endToEndTesting/src/test/resources/languageTestFiles/java/sortAlgo`. -It is important to note that the language identifier must match `Language#getIdentifier` to load the language during testing correctly. - -To automatically generate expected results, the test in `EndToEndGeneratorTest` can be executed to generate a JSON result description file. This file has to be copied to `JPlag/jplag.endToEndTesting/src/test/resources/results//.json`. -Once the test data has been copied, the end-to-end tests can be successfully tested. As soon as a change in the detection takes place, the results will differ from the stored results, and the tests will fail if the results have changed. - -### Extending The Comparison Value -As already described, the current comparisons in the end-to-end test treat the values of `minimal similarity`, `maximum similarity`, and `matched token number`. -As soon as there is a need to extend these comparison values, this section describes how this can be achieved. -Beforehand, however, this should be discussed in a new issue about this need. - -- For new comparison values, these properties must be extended in the `ExpectedResult` record at the package `de.jplag.endtoend.model`. Here it is sufficient to add the values in the record and to enter the JSON name as `@JsonProperty("json_name")`. - -```JAVA -public record ExpectedResult( - @JsonProperty("minimal_similarity") float resultSimilarityMinimum, - @JsonProperty("maximum_similarity") float resultSimilarityMaximum, - @JsonProperty("matched_token_number") int resultMatchedTokenNumber) { -} -``` - -- To include the new value in the tests, they must be added to the `EndToEndSuiteTest` as a comparison operation at the package `de.jplag.endtoend`. The `runJPlagTestSuite()` function provided for this purpose must be extended to include the new comparison value. To do this, create the comparison as shown in the code example below. - -```JAVA -//... - if (areDoublesDifferent(result.resultSimilarityMaximum(), jPlagComparison.maximalSimilarity())) { - addToValidationErrors("maximal similarity", String.valueOf(result.resultSimilarityMaximum()), - String.valueOf(jPlagComparison.maximalSimilarity())); - } -//... -``` - -- Once the tests run the first time, they will fail due to the missing values in the old JSON result file used for the test cases. The old results must then be replaced with new ones. -For this purpose, the last section of the chapter [Copying Plagiarism To The Resources](#copying-plagiarism-to-the-resources) can help. - -### Extending JPlag Test Run Options -The end-to-end tests support the possible scan options of the JPlag API. Currently, `minimumTokenMatch` is used in the end-to-end tests. These values are also stored in the JSON as configuration to keep the test cases at the options apart. Likewise, also changes in the logic of the different options are to be determined to be able. - -- To extend new options to the end-to-end tests, they must be added to the record object `Options` in the package `de.jplag.endtoend.model`. Here it is sufficient to add the values in the record and to enter the JSON name as `@JsonProperty("json_name")`. - -```JAVA -public record Options( -@JsonProperty("minimum_token_match") Integer minimumTokenMatch) { -} -``` - -- After the new value has been added to the record, the creation of the object must now also be adjusted in the `EndToEndSuiteTest`. The 'setRunOptions' function is provided for this purpose. The options can be added in any order and combination. It should be noted that each test case is run with these options. - -```JAVA - private void setRunOptions() { - options = new ArrayList<>(); - options.add(new Options(1)); - options.add(new Options(15)); - } -``` - -- If you want to create individual test cases by testing the options only on a specific dataset, a new test case must be created for this purpose. The transfer parameter options can be adjusted and specified for the new test cases. This can then be tested with the function `runTests`. - ```JAVA - runTests(directoryName, option, currentLanguageIdentifier, testCase, currentResultDescription); -``` +## References +These elaborations provide basic ideas on how a modification of the plagiarized source code can look or be adapted. +These code adaptations refer to various changes, from +adding/removing comments to architectural changes in the deliverables. -- Once the tests run the first time, they will fail due to the missing values in the old JSON result file used for the test cases. The old results must then be replaced with new ones. -For this purpose, the last section of the chapter [Copying Plagiarism To The Resources](#copying-plagiarism-to-the-resources) can be used as help. +The following elaborations were used to be able to create the plagiarisms with the broadest coverage: +- [Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim](https://ieeexplore.ieee.org/abstract/document/7910274 "Detecting Source Code Plagiarism on Introductory Programming Course Assignments Using a Bytecode Approach - Oscar Karnalim") +- [Detecting Disguised Plagiarism - Hatem A. Mahmoud](https://arxiv.org/abs/1711.02149 "Detecting Disguised Plagiarism - Hatem A. Mahmoud") +- [Instructor-centric source code plagiarism detection and plagiarism corpus](https://dl.acm.org/doi/abs/10.1145/2325296.2325328 "Instructor-centric source code plagiarism detection and plagiarism corpus") diff --git a/gitHooks/hooks/pre-commit b/gitHooks/hooks/pre-commit new file mode 100755 index 0000000000..7796838649 --- /dev/null +++ b/gitHooks/hooks/pre-commit @@ -0,0 +1,42 @@ +#!/bin/sh + +echo "Running pre commit checks" + +hasJavaFiles=0 +hasJsFiles=0 + +files=`git diff --name-only --cached` +while read name +do + if [[ $name == report-viewer* ]] + then + hasJsFiles=1 + fi + + if [[ $name == *.java ]] + then + hasJavaFiles=1 + fi +done <<< "$files" + +if [[ $hasJsFiles -gt 0 ]] +then + echo "Running report viewer pre commit checks" + cd report-viewer + ../gitHooks/scripts/reportViewerPreCommit + if [ $? -gt 0 ] + then + exit 1 + fi + cd .. +fi + +if [[ $hasJavaFiles -gt 0 ]] +then + echo "Running java pre commit checks" + gitHooks/scripts/javaPreCommit + if [ $? -gt 0 ] + then + exit 1 + fi +fi diff --git a/gitHooks/scripts/javaPreCommit b/gitHooks/scripts/javaPreCommit new file mode 100755 index 0000000000..19076694d7 --- /dev/null +++ b/gitHooks/scripts/javaPreCommit @@ -0,0 +1,19 @@ +#!/bin/bash + +if ! command -v mvn &> /dev/null #checks if maven is installed +then + echo "Maven is not installed. Spotless will not be checked" + exit 0 +fi + +#prevents the shell from aborting if maven returns a non zero exit code +set +e +echo Checking spotless +mvn spotless:check &> /dev/null +exitCode=$? + +if [ $exitCode -gt 0 ] +then + echo "Spotless failed. Please run 'mvn spotless:apply' to fix." +fi +exit $exitCode diff --git a/gitHooks/scripts/reportViewerPreCommit b/gitHooks/scripts/reportViewerPreCommit new file mode 100755 index 0000000000..502b47cb33 --- /dev/null +++ b/gitHooks/scripts/reportViewerPreCommit @@ -0,0 +1,9 @@ +#!/bin/bash + +if ! command -v npm &> /dev/null #checks if npm is installed +then + echo "Npm is not installed. Linter check will be skipped" + exit 0 +fi + +npx lint-staged diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrParserAdapter.java b/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrParserAdapter.java index 8ba51cea0e..440d4a7aaf 100644 --- a/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrParserAdapter.java +++ b/language-antlr-utils/src/main/java/de/jplag/antlr/AbstractAntlrParserAdapter.java @@ -62,6 +62,8 @@ private void parseFile(File file, TokenCollector collector) throws ParsingExcept Lexer lexer = this.createLexer(CharStreams.fromReader(reader)); CommonTokenStream tokenStream = new CommonTokenStream(lexer); T parser = this.createParser(tokenStream); + parser.removeErrorListeners(); + parser.addErrorListener(new AntlrLoggerErrorListener()); ParserRuleContext entryContext = this.getEntryContext(parser); ParseTreeWalker treeWalker = new ParseTreeWalker(); InternalListener listener = new InternalListener(this.getListener(), collector); diff --git a/language-antlr-utils/src/main/java/de/jplag/antlr/AntlrLoggerErrorListener.java b/language-antlr-utils/src/main/java/de/jplag/antlr/AntlrLoggerErrorListener.java new file mode 100644 index 0000000000..2fa820ca64 --- /dev/null +++ b/language-antlr-utils/src/main/java/de/jplag/antlr/AntlrLoggerErrorListener.java @@ -0,0 +1,21 @@ +package de.jplag.antlr; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Writes error messages from ANTLR to a logger + */ +public class AntlrLoggerErrorListener extends BaseErrorListener { + private static final Logger logger = LoggerFactory.getLogger(AntlrLoggerErrorListener.class); + private static final String ERROR_TEMPLATE = "ANTLR error - line {}:{} {}"; + + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, + RecognitionException e) { + logger.error(ERROR_TEMPLATE, line, charPositionInLine, msg); + } +} diff --git a/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java b/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java index 2eb99262d5..7da3304a92 100644 --- a/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java +++ b/language-api/src/main/java/de/jplag/semantics/CodeSemantics.java @@ -7,11 +7,11 @@ import java.util.Set; /** - * Contains semantic information about a code snippet, in our case either a token or a statement. + * Contains semantic information about a code fragment, in our case either a token or a statement. */ public class CodeSemantics { - private boolean keep; + private boolean critical; private PositionSignificance positionSignificance; private final int bidirectionalBlockDepthChange; private final Set reads; @@ -19,47 +19,47 @@ public class CodeSemantics { /** * Creates new semantics. reads and writes, which each contain the variables which were (potentially) read from/written - * to in this code snippet, are created empty. - * @param keep Whether the code snippet must be kept or if it may be removed. - * @param positionSignificance In which way the position of the code snippet relative to other code snippets of the same - * type is significant. For the possible options see {@link PositionSignificance}. - * @param bidirectionalBlockDepthChange How the code snippet affects the depth of bidirectional blocks, meaning blocks + * to in this code fragment, are created empty. + * @param critical Whether the code fragment must be kept as it affects the program behavior or if it may be removed. + * @param positionSignificance In which way the position of the code fragment relative to other tokens of the same type + * is significant. For the possible options see {@link PositionSignificance}. + * @param bidirectionalBlockDepthChange How the code fragment affects the depth of bidirectional blocks, meaning blocks * where any statement within it may be executed after any other. This will typically be a loop. - * @param reads A set of the variables which were (potentially) read from in the code snippet. - * @param writes A set of the variables which were (potentially) written to in the code snippet. + * @param reads A set of the variables which were (potentially) read from in the code fragment. + * @param writes A set of the variables which were (potentially) written to in the code fragment. */ - private CodeSemantics(boolean keep, PositionSignificance positionSignificance, int bidirectionalBlockDepthChange, Set reads, + private CodeSemantics(boolean critical, PositionSignificance positionSignificance, int bidirectionalBlockDepthChange, Set reads, Set writes) { - this.keep = keep; + this.critical = critical; this.positionSignificance = positionSignificance; this.bidirectionalBlockDepthChange = bidirectionalBlockDepthChange; this.reads = reads; this.writes = writes; } - private CodeSemantics(boolean keep, PositionSignificance positionSignificance, int bidirectionalBlockDepthChange) { - this(keep, positionSignificance, bidirectionalBlockDepthChange, new HashSet<>(), new HashSet<>()); + private CodeSemantics(boolean critical, PositionSignificance positionSignificance, int bidirectionalBlockDepthChange) { + this(critical, positionSignificance, bidirectionalBlockDepthChange, new HashSet<>(), new HashSet<>()); } /** - * Creates new semantics with the following meaning: The code snippet may be removed, and its position relative to other - * code snippets may change. Example: An assignment to a local variable. + * Creates new semantics with the following meaning: The code fragment may be removed, and its position relative to + * other code fragments may change. Example: An assignment to a local variable. */ public CodeSemantics() { this(false, PositionSignificance.NONE, 0); } /** - * @return new semantics with the following meaning: The code snippet may not be removed, and its position relative to - * other code snippets may change. Example: An attribute declaration. + * @return new semantics with the following meaning: The code fragment may not be removed, and its position relative to + * other code fragments may change. Example: An attribute declaration. */ public static CodeSemantics createKeep() { return new CodeSemantics(true, PositionSignificance.NONE, 0); } /** - * @return new semantics with the following meaning: The code snippet may not be removed, and its position must stay - * invariant to other code snippets of the same type. Example: A method call which is guaranteed to not result in an + * @return new semantics with the following meaning: The code fragment may not be removed, and its position must stay + * invariant to other code fragments of the same type. Example: A method call which is guaranteed to not result in an * exception. */ public static CodeSemantics createCritical() { @@ -67,16 +67,16 @@ public static CodeSemantics createCritical() { } /** - * @return new semantics with the following meaning: The code snippet may not be removed, and its position must stay - * invariant to all other code snippets. Example: A return statement. + * @return new semantics with the following meaning: The code fragment may not be removed, and its position must stay + * invariant to all other code fragments. Example: A return statement. */ public static CodeSemantics createControl() { return new CodeSemantics(true, PositionSignificance.FULL, 0); } /** - * @return new semantics with the following meaning: The code snippet may not be removed, and its position must stay - * invariant to all other code snippets, which also begins a bidirectional block. Example: The beginning of a while + * @return new semantics with the following meaning: The code fragment may not be removed, and its position must stay + * invariant to all other code fragments, which also begins a bidirectional block. Example: The beginning of a while * loop. */ public static CodeSemantics createLoopBegin() { @@ -84,71 +84,71 @@ public static CodeSemantics createLoopBegin() { } /** - * @return new semantics with the following meaning: The code snippet may not be removed, and its position must stay - * invariant to all other code snippets, which also ends a bidirectional block. Example: The end of a while loop. + * @return new semantics with the following meaning: The code fragment may not be removed, and its position must stay + * invariant to all other code fragments, which also ends a bidirectional block. Example: The end of a while loop. */ public static CodeSemantics createLoopEnd() { return new CodeSemantics(true, PositionSignificance.FULL, -1); } /** - * @return whether this code snippet must be kept. + * @return whether this token is critical to the program behavior. */ - public boolean keep() { - return keep; + public boolean isCritical() { + return critical; } /** - * Mark this code snippet as having to be kept. + * Mark this token as critical to the program behavior. */ - public void markKeep() { - keep = true; + public void markAsCritical() { + critical = true; } /** - * @return the change this code snippet causes in the depth of bidirectional loops. + * @return the change this code fragment causes in the depth of bidirectional loops. */ public int bidirectionalBlockDepthChange() { return bidirectionalBlockDepthChange; } /** - * @return whether this code snippet has partial position significance. + * @return whether this code fragment has partial position significance. */ public boolean hasPartialPositionSignificance() { return positionSignificance == PositionSignificance.PARTIAL; } /** - * @return whether this code snippet has full position significance. + * @return whether this code fragment has full position significance. */ public boolean hasFullPositionSignificance() { return positionSignificance == PositionSignificance.FULL; } /** - * Mark this code snippet as having full position significance. + * Mark this code fragment as having full position significance. */ public void markFullPositionSignificance() { positionSignificance = PositionSignificance.FULL; } /** - * @return an unmodifiable set of the variables which were (potentially) read from in this code snippet. + * @return an unmodifiable set of the variables which were (potentially) read from in this code fragment. */ public Set reads() { return Collections.unmodifiableSet(reads); } /** - * @return an unmodifiable set of the variables which were (potentially) written to in this code snippet. + * @return an unmodifiable set of the variables which were (potentially) written to in this code fragment. */ public Set writes() { return Collections.unmodifiableSet(writes); } /** - * Add a variable to the set of variables which were (potentially) read from in this code snippet. + * Add a variable to the set of variables which were (potentially) read from in this code fragment. * @param variable The variable which is added. */ public void addRead(Variable variable) { @@ -156,7 +156,7 @@ public void addRead(Variable variable) { } /** - * Add a variable to the set of variables which were (potentially) written to in this code snippet. + * Add a variable to the set of variables which were (potentially) written to in this code fragment. * @param variable The variable which is added. */ public void addWrite(Variable variable) { @@ -182,7 +182,7 @@ public static CodeSemantics join(List semanticsList) { Set reads = new HashSet<>(); Set writes = new HashSet<>(); for (CodeSemantics semantics : semanticsList) { - keep = keep || semantics.keep; + keep = keep || semantics.critical; if (semantics.positionSignificance.compareTo(positionSignificance) > 0) { positionSignificance = semantics.positionSignificance; } @@ -196,7 +196,7 @@ public static CodeSemantics join(List semanticsList) { @Override public String toString() { List properties = new LinkedList<>(); - if (keep) { + if (critical) { properties.add("keep"); } if (positionSignificance != PositionSignificance.NONE) { diff --git a/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java b/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java index 36d997f7be..f659ba2b22 100644 --- a/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java +++ b/language-testutils/src/test/java/de/jplag/testutils/LanguageModuleTest.java @@ -14,6 +14,7 @@ import java.util.Collection; import java.util.List; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -32,6 +33,7 @@ import de.jplag.testutils.datacollector.TestData; import de.jplag.testutils.datacollector.TestDataCollector; import de.jplag.testutils.datacollector.TestSourceIgnoredLinesCollector; +import de.jplag.testutils.datacollector.TokenPositionTestData; /** * Base class for language module tests. Automatically adds all common tests types for jplag languages. @@ -174,7 +176,7 @@ final List testTokensContainedData() { final void testTokenSequence(TestDataCollector.TokenListTest test) throws ParsingException, IOException { List actual = extractTokenTypes(test.data()); List expected = new ArrayList<>(test.tokens()); - if (expected.get(expected.size() - 1) != SharedTokenType.FILE_END) { + if (expected.getLast() != SharedTokenType.FILE_END) { expected.add(SharedTokenType.FILE_END); } assertTokensMatch(expected, actual, "Extracted token from " + test.data().describeTestSource() + " does not match expected sequence."); @@ -196,6 +198,45 @@ final List testTokenSequenceData() { return ignoreEmptyTestType(this.collector.getTokenSequenceTest()); } + /** + * Tests if the tokens specified for the token position tests are present in the sources + * @param testData The specifications of the expected tokens and the test source + * @throws ParsingException If the parsing fails + * @throws IOException If IO operations fail. If this happens, that should be unrelated to the test itself. + */ + @ParameterizedTest + @MethodSource("getTokenPositionTestData") + @DisplayName("Tests if the extracted tokens contain the tokens specified in the test files.") + final void testTokenPositions(TokenPositionTestData testData) throws ParsingException, IOException { + List extractedTokens = parseTokens(testData); + List failedTokens = new ArrayList<>(); + + for (TokenPositionTestData.TokenData expectedToken : testData.getExpectedTokens()) { + TokenType expectedType = this.languageTokens.stream().filter(type -> type.toString().equals(expectedToken.typeName())).findFirst() + .orElseThrow(() -> new IOException(String.format("The token type %s does not exist.", expectedToken.typeName()))); + + if (extractedTokens.stream().noneMatch(token -> token.getType() == expectedType && token.getLine() == expectedToken.lineNumber() + && token.getColumn() == expectedToken.columnNumber() && token.getLength() == expectedToken.length())) { + failedTokens.add(expectedToken); + } + } + + if (!failedTokens.isEmpty()) { + String failureDescriptors = String.join(System.lineSeparator(), + failedTokens.stream().map( + token -> token.typeName() + " at (" + token.lineNumber() + ":" + token.columnNumber() + ") with length " + token.length()) + .toList()); + fail("Some tokens weren't extracted with the correct properties:" + System.lineSeparator() + failureDescriptors); + } + } + + /** + * @return All token positions tests that are configured + */ + final List getTokenPositionTestData() { + return ignoreEmptyTestType(this.collector.getTokenPositionTestData()); + } + /** * Tests all configured test sources for a monotone order of tokens * @param data The test source @@ -231,8 +272,7 @@ final void testMonotoneTokenOrder(TestData data) throws ParsingException, IOExce final void testTokenSequencesEndsWithFileEnd(TestData data) throws ParsingException, IOException { List tokens = parseTokens(data); - assertEquals(SharedTokenType.FILE_END, tokens.get(tokens.size() - 1).getType(), - "Last token in " + data.describeTestSource() + " is not file end."); + assertEquals(SharedTokenType.FILE_END, tokens.getLast().getType(), "Last token in " + data.describeTestSource() + " is not file end."); } /** @@ -251,6 +291,11 @@ final void collectTestData() { collectTestData(this.collector); } + @AfterAll + final void deleteTemporaryFiles() { + TemporaryFileHolder.deleteTemporaryFiles(); + } + private List parseTokens(TestData source) throws ParsingException, IOException { List tokens = source.parseTokens(this.language); logger.info(TokenPrinter.printTokens(tokens)); diff --git a/language-testutils/src/test/java/de/jplag/testutils/TemporaryFileHolder.java b/language-testutils/src/test/java/de/jplag/testutils/TemporaryFileHolder.java new file mode 100644 index 0000000000..98a95fb7fb --- /dev/null +++ b/language-testutils/src/test/java/de/jplag/testutils/TemporaryFileHolder.java @@ -0,0 +1,20 @@ +package de.jplag.testutils; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Stores all temporary files that are created for a {@link LanguageModuleTest} and provides the option to delete them + */ +public class TemporaryFileHolder { + public static List temporaryFiles = new ArrayList<>(); + + /** + * Deletes all temporary files that have been created up to this point + */ + public static void deleteTemporaryFiles() { + temporaryFiles.forEach(File::delete); + temporaryFiles.clear(); + } +} diff --git a/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java b/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java index 8d93f7e156..6a87110234 100644 --- a/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java +++ b/language-testutils/src/test/java/de/jplag/testutils/datacollector/InlineTestData.java @@ -8,6 +8,7 @@ import de.jplag.Language; import de.jplag.ParsingException; import de.jplag.Token; +import de.jplag.testutils.TemporaryFileHolder; import de.jplag.util.FileUtils; /** @@ -25,7 +26,7 @@ public List parseTokens(Language language) throws ParsingException, IOExc File file = File.createTempFile("testSource", language.suffixes()[0]); FileUtils.write(file, this.testData); List tokens = language.parse(Collections.singleton(file)); - file.delete(); + TemporaryFileHolder.temporaryFiles.add(file); return tokens; } diff --git a/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java index d5a929d06f..9b45e8d8b6 100644 --- a/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java +++ b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TestDataCollector.java @@ -1,10 +1,13 @@ package de.jplag.testutils.datacollector; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -18,6 +21,7 @@ public class TestDataCollector { private final List tokenCoverageData; private final List containedTokenData; private final List tokenSequenceTest; + private final List tokenPositionTestData; private final List allTestData; @@ -34,6 +38,7 @@ public TestDataCollector(File testFileLocation) { this.tokenCoverageData = new ArrayList<>(); this.containedTokenData = new ArrayList<>(); this.tokenSequenceTest = new ArrayList<>(); + this.tokenPositionTestData = new ArrayList<>(); this.allTestData = new ArrayList<>(); } @@ -73,6 +78,28 @@ public TestDataContext inlineSource(String... sources) { return new TestDataContext(data); } + /** + * Adds all files from the given directory for token position tests. The sources can still be used for other tests, + * using the returned {@link TestDataContext} + * @param directoryName The name of the directory containing the token position tests. + * @return The context containing the added sources + * @throws IOException If the files cannot be read + */ + public TestDataContext addTokenPositionTests(String directoryName) { + File directory = new File(this.testFileLocation, directoryName); + Set allTestsInDirectory = new HashSet<>(); + for (File file : Objects.requireNonNull(directory.listFiles())) { + try { + TokenPositionTestData data = new TokenPositionTestData(file); + allTestsInDirectory.add(data); + this.tokenPositionTestData.add(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new TestDataContext(allTestsInDirectory); + } + /** * @return The test data that should be checked for source coverage */ @@ -101,6 +128,10 @@ public List getTokenSequenceTest() { return Collections.unmodifiableList(tokenSequenceTest); } + public List getTokenPositionTestData() { + return Collections.unmodifiableList(this.tokenPositionTestData); + } + /** * @return The list of all test data */ diff --git a/language-testutils/src/test/java/de/jplag/testutils/datacollector/TokenPositionTestData.java b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TokenPositionTestData.java new file mode 100644 index 0000000000..fb18c5deb0 --- /dev/null +++ b/language-testutils/src/test/java/de/jplag/testutils/datacollector/TokenPositionTestData.java @@ -0,0 +1,99 @@ +package de.jplag.testutils.datacollector; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import de.jplag.Language; +import de.jplag.ParsingException; +import de.jplag.Token; +import de.jplag.testutils.TemporaryFileHolder; +import de.jplag.util.FileUtils; + +/** + * Test sources with token information Reads token position test specifications form a file and provides the token + * information for tests. The sources can be used as regular test sources. + */ +public class TokenPositionTestData implements TestData { + private final List sourceLines; + private final List expectedTokens; + + private final String descriptor; + private final String fileName; + + /** + * @param testFile The file containing the test specifications + * @throws IOException If the file cannot be read + */ + public TokenPositionTestData(File testFile) throws IOException { + this.sourceLines = new ArrayList<>(); + this.expectedTokens = new ArrayList<>(); + this.descriptor = "(Token position file: " + testFile.getName() + ")"; + this.fileName = testFile.getName(); + this.readFile(testFile); + } + + private void readFile(File testFile) throws IOException { + List testFileLines = FileUtils.readFileContent(testFile).lines().toList(); + int currentLine = 0; + + for (String sourceLine : testFileLines) { + if (sourceLine.charAt(0) == '>') { + this.sourceLines.add(sourceLine.substring(1)); + currentLine++; + } + + if (sourceLine.charAt(0) == '$') { + int column = sourceLine.indexOf('|'); + String[] tokenDescriptionParts = sourceLine.split(" ", 0); + + String typeName = tokenDescriptionParts[tokenDescriptionParts.length - 2]; + int length = Integer.parseInt(tokenDescriptionParts[tokenDescriptionParts.length - 1]); + this.expectedTokens.add(new TokenData(typeName, currentLine, column, length)); + } + } + } + + @Override + public List parseTokens(Language language) throws ParsingException, IOException { + File file = File.createTempFile("testSource", language.suffixes()[0]); + FileUtils.write(file, String.join(System.lineSeparator(), sourceLines)); + List tokens = language.parse(Collections.singleton(file)); + TemporaryFileHolder.temporaryFiles.add(file); + return tokens; + } + + @Override + public String[] getSourceLines() { + return this.sourceLines.toArray(new String[0]); + } + + @Override + public String describeTestSource() { + return this.descriptor; + } + + /** + * @return A list of the expected tokens for this test source + */ + public List getExpectedTokens() { + return expectedTokens; + } + + /** + * Information about a single token + * @param typeName The name of the token type + * @param lineNumber The line the token is in (1 based) + * @param columnNumber The column the token is in (1 based) + * @param length The length of the token + */ + public record TokenData(String typeName, int lineNumber, int columnNumber, int length) { + } + + @Override + public String toString() { + return this.fileName; + } +} diff --git a/languages/c/src/main/java/de/jplag/c/CLanguage.java b/languages/c/src/main/java/de/jplag/c/CLanguage.java index ba99dbf498..c55dbf60b3 100644 --- a/languages/c/src/main/java/de/jplag/c/CLanguage.java +++ b/languages/c/src/main/java/de/jplag/c/CLanguage.java @@ -12,6 +12,7 @@ @MetaInfServices(de.jplag.Language.class) public class CLanguage implements Language { + private static final String NAME = "C"; private static final String IDENTIFIER = "c"; private final Scanner scanner; // c code is scanned not parsed @@ -27,7 +28,7 @@ public String[] suffixes() { @Override public String getName() { - return "C Scanner"; + return NAME; } @Override diff --git a/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java b/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java index c08e53dce1..b76dfc823e 100644 --- a/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java +++ b/languages/cpp/src/main/java/de/jplag/cpp/CPPLanguage.java @@ -10,6 +10,7 @@ */ @MetaInfServices(Language.class) public class CPPLanguage extends AbstractAntlrLanguage { + private static final String NAME = "C++"; private static final String IDENTIFIER = "cpp"; public CPPLanguage() { @@ -23,7 +24,7 @@ public String[] suffixes() { @Override public String getName() { - return "C++ Parser"; + return NAME; } @Override diff --git a/languages/cpp/src/main/java/de/jplag/cpp/CPPListener.java b/languages/cpp/src/main/java/de/jplag/cpp/CPPListener.java index 6bfa81c098..9cea3eed7f 100644 --- a/languages/cpp/src/main/java/de/jplag/cpp/CPPListener.java +++ b/languages/cpp/src/main/java/de/jplag/cpp/CPPListener.java @@ -100,10 +100,11 @@ class CPPListener extends AbstractAntlrListener { CPPListener() { - visit(ClassSpecifierContext.class, rule -> rule.classHead().Union() != null).map(UNION_BEGIN, UNION_END).withSemantics(CodeSemantics::new); + visit(ClassSpecifierContext.class, rule -> rule.classHead().Union() != null).map(UNION_BEGIN, UNION_END).addClassScope() + .withSemantics(CodeSemantics::createControl); mapClass(ClassKeyContext::Class, CLASS_BEGIN, CLASS_END); mapClass(ClassKeyContext::Struct, STRUCT_BEGIN, STRUCT_END); // structs are basically just classes - visit(EnumSpecifierContext.class).map(ENUM_BEGIN, ENUM_END).withSemantics(CodeSemantics::createControl); + visit(EnumSpecifierContext.class).map(ENUM_BEGIN, ENUM_END).addClassScope().withSemantics(CodeSemantics::createControl); visit(FunctionDefinitionContext.class).map(FUNCTION_BEGIN, FUNCTION_END).addLocalScope().withSemantics(CodeSemantics::createControl); diff --git a/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java b/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java index aeeb53728f..67d24c60aa 100644 --- a/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java +++ b/languages/csharp/src/main/java/de/jplag/csharp/CSharpLanguage.java @@ -9,7 +9,7 @@ */ @MetaInfServices(de.jplag.Language.class) public class CSharpLanguage extends AbstractAntlrLanguage { - private static final String NAME = "C# 6 Parser"; + private static final String NAME = "C#"; private static final String IDENTIFIER = "csharp"; private static final String[] FILE_ENDINGS = new String[] {".cs", ".CS"}; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; diff --git a/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java b/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java index e14926b43e..cb2d08eace 100644 --- a/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java +++ b/languages/golang/src/main/java/de/jplag/golang/GoLanguage.java @@ -6,7 +6,7 @@ @MetaInfServices(de.jplag.Language.class) public class GoLanguage extends AbstractAntlrLanguage { - private static final String NAME = "Go Parser"; + private static final String NAME = "Go"; private static final String IDENTIFIER = "go"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSIONS = {".go"}; diff --git a/languages/java/src/main/java/de/jplag/java/JavaLanguage.java b/languages/java/src/main/java/de/jplag/java/JavaLanguage.java index 4db88ef015..79d32f502c 100644 --- a/languages/java/src/main/java/de/jplag/java/JavaLanguage.java +++ b/languages/java/src/main/java/de/jplag/java/JavaLanguage.java @@ -14,8 +14,8 @@ */ @MetaInfServices(de.jplag.Language.class) public class JavaLanguage implements de.jplag.Language { + private static final String NAME = "Java"; private static final String IDENTIFIER = "java"; - public static final int JAVA_VERSION = 21; private final Parser parser; @@ -30,7 +30,7 @@ public String[] suffixes() { @Override public String getName() { - return "Javac based AST plugin"; + return NAME; } @Override diff --git a/languages/java/src/main/java/de/jplag/java/JavacAdapter.java b/languages/java/src/main/java/de/jplag/java/JavacAdapter.java index 1815c4b811..0af39bf6b6 100644 --- a/languages/java/src/main/java/de/jplag/java/JavacAdapter.java +++ b/languages/java/src/main/java/de/jplag/java/JavacAdapter.java @@ -29,6 +29,10 @@ public class JavacAdapter { + private static final String NO_ANNOTATION_PROCESSING = "-proc:none"; + private static final String PREVIEW_FLAG = "--enable-preview"; + private static final String RELEASE_VERSION_OPTION = "--release="; + private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); public void parseFiles(Set files, final Parser parser) throws ParsingException { @@ -39,10 +43,10 @@ public void parseFiles(Set files, final Parser parser) throws ParsingExcep try (final StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, null, guessedCharset)) { var javaFiles = fileManager.getJavaFileObjectsFromFiles(files); - // We need to disable annotation processing, see - // https://stackoverflow.com/questions/72737445/system-java-compiler-behaves-different-depending-on-dependencies-defined-in-mave - final CompilationTask task = compiler.getTask(null, fileManager, listener, - List.of("-proc:none", "--enable-preview", "--release=" + JavaLanguage.JAVA_VERSION), null, javaFiles); + // We need to disable annotation processing, see https://stackoverflow.com/q/72737445 + String releaseVersion = RELEASE_VERSION_OPTION + Runtime.version().feature(); // required for preview flag + List options = List.of(NO_ANNOTATION_PROCESSING, PREVIEW_FLAG, releaseVersion); + final CompilationTask task = compiler.getTask(null, fileManager, listener, options, null, javaFiles); final Trees trees = Trees.instance(task); final SourcePositions positions = new FixedSourcePositions(trees.getSourcePositions()); for (final CompilationUnitTree ast : executeCompilationTask(task, parser.logger)) { diff --git a/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java b/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java index 28bd5838a0..9589da81b7 100644 --- a/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java +++ b/languages/java/src/main/java/de/jplag/java/TokenGeneratingTreeScanner.java @@ -380,10 +380,11 @@ public Void visitThrow(ThrowTree node, Void unused) { @Override public Void visitNewClass(NewClassTree node, Void unused) { long start = positions.getStartPosition(ast, node); + long end = positions.getEndPosition(ast, node.getIdentifier()); if (!node.getTypeArguments().isEmpty()) { addToken(JavaTokenType.J_GENERIC, start, 3 + node.getIdentifier().toString().length(), new CodeSemantics()); } - addToken(JavaTokenType.J_NEWCLASS, start, 3, new CodeSemantics()); + addToken(JavaTokenType.J_NEWCLASS, start, end, new CodeSemantics()); super.visitNewClass(node, null); return null; } @@ -399,8 +400,8 @@ public Void visitTypeParameter(TypeParameterTree node, Void unused) { @Override public Void visitNewArray(NewArrayTree node, Void unused) { long start = positions.getStartPosition(ast, node); - long end = positions.getEndPosition(ast, node) - 1; - addToken(JavaTokenType.J_NEWARRAY, start, 3, new CodeSemantics()); + long end = node.getType() == null ? start + 1 : positions.getEndPosition(ast, node.getType()); + addToken(JavaTokenType.J_NEWARRAY, start, end, new CodeSemantics()); scan(node.getType(), null); scan(node.getDimensions(), null); boolean hasInit = node.getInitializers() != null && !node.getInitializers().isEmpty(); @@ -411,6 +412,7 @@ public Void visitNewArray(NewArrayTree node, Void unused) { scan(node.getInitializers(), null); // super method has annotation processing but we have it disabled anyways if (hasInit) { + end = positions.getEndPosition(ast, node.getInitializers().getLast()) - 1; addToken(JavaTokenType.J_ARRAY_INIT_END, end, 1, new CodeSemantics()); } return null; @@ -419,7 +421,8 @@ public Void visitNewArray(NewArrayTree node, Void unused) { @Override public Void visitAssignment(AssignmentTree node, Void unused) { long start = positions.getStartPosition(ast, node); - addToken(JavaTokenType.J_ASSIGN, start, 1, new CodeSemantics()); + long end = positions.getStartPosition(ast, node.getExpression()) - 1; + addToken(JavaTokenType.J_ASSIGN, start, end, new CodeSemantics()); variableRegistry.setNextVariableAccessType(VariableAccessType.WRITE); return super.visitAssignment(node, null); } @@ -427,7 +430,8 @@ public Void visitAssignment(AssignmentTree node, Void unused) { @Override public Void visitCompoundAssignment(CompoundAssignmentTree node, Void unused) { long start = positions.getStartPosition(ast, node); - addToken(JavaTokenType.J_ASSIGN, start, 1, new CodeSemantics()); + long end = positions.getStartPosition(ast, node.getExpression()) - 1; + addToken(JavaTokenType.J_ASSIGN, start, end, new CodeSemantics()); variableRegistry.setNextVariableAccessType(VariableAccessType.READ_WRITE); return super.visitCompoundAssignment(node, null); } @@ -437,7 +441,7 @@ public Void visitUnary(UnaryTree node, Void unused) { if (Set.of(Tree.Kind.PREFIX_INCREMENT, Tree.Kind.POSTFIX_INCREMENT, Tree.Kind.PREFIX_DECREMENT, Tree.Kind.POSTFIX_DECREMENT) .contains(node.getKind())) { long start = positions.getStartPosition(ast, node); - addToken(JavaTokenType.J_ASSIGN, start, 1, new CodeSemantics()); + addToken(JavaTokenType.J_ASSIGN, start, node.toString().length(), new CodeSemantics()); variableRegistry.setNextVariableAccessType(VariableAccessType.READ_WRITE); } return super.visitUnary(node, null); @@ -454,6 +458,8 @@ public Void visitAssert(AssertTree node, Void unused) { public Void visitVariable(VariableTree node, Void unused) { if (!node.getName().contentEquals(ANONYMOUS_VARIABLE_NAME)) { long start = positions.getStartPosition(ast, node); + long end = positions.getEndPosition(ast, node) - 1; + end -= node.getInitializer() == null ? 0 : node.getInitializer().toString().length(); String name = node.getName().toString(); boolean inLocalScope = variableRegistry.inLocalScope(); // this presents a problem when classes are declared in local scopes, which can happen in ad-hoc implementations @@ -465,7 +471,7 @@ public Void visitVariable(VariableTree node, Void unused) { } else { semantics = CodeSemantics.createKeep(); } - addToken(JavaTokenType.J_VARDEF, start, node.toString().length(), semantics); + addToken(JavaTokenType.J_VARDEF, start, end, semantics); // manually add variable to semantics since identifier isn't visited variableRegistry.setNextVariableAccessType(VariableAccessType.WRITE); variableRegistry.registerVariableAccess(name, !inLocalScope); @@ -483,7 +489,7 @@ public Void visitConditionalExpression(ConditionalExpressionTree node, Void unus @Override public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { long start = positions.getStartPosition(ast, node); - long end = positions.getEndPosition(ast, node.getMethodSelect()) - start; + long end = positions.getEndPosition(ast, node.getMethodSelect()); CodeSemantics codeSemantics = CRITICAL_METHODS.contains(node.getMethodSelect().toString()) ? CodeSemantics.createCritical() : CodeSemantics.createControl(); addToken(JavaTokenType.J_APPLY, start, end, codeSemantics); diff --git a/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java b/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java index e54955781a..a4f89d9bb0 100644 --- a/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java +++ b/languages/java/src/test/java/de/jplag/java/JavaLanguageTest.java @@ -73,6 +73,8 @@ protected void collectTestData(TestDataCollector collector) { collector.testFile("AnonymousVariables.java").testTokenSequence(J_CLASS_BEGIN, J_METHOD_BEGIN, J_VARDEF, J_IF_BEGIN, J_IF_END, J_METHOD_END, J_CLASS_END); + + collector.addTokenPositionTests("tokenPositions"); } @Override diff --git a/languages/java/src/test/resources/de/jplag/java/tokenPositions/VarDef_1.java b/languages/java/src/test/resources/de/jplag/java/tokenPositions/VarDef_1.java new file mode 100644 index 0000000000..276f17351b --- /dev/null +++ b/languages/java/src/test/resources/de/jplag/java/tokenPositions/VarDef_1.java @@ -0,0 +1,4 @@ +>class Test { +> int test; +$ | J_VARDEF 8 +>} diff --git a/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java b/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java index 487effaa24..9b7678681e 100644 --- a/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java +++ b/languages/kotlin/src/main/java/de/jplag/kotlin/KotlinLanguage.java @@ -10,7 +10,7 @@ @MetaInfServices(de.jplag.Language.class) public class KotlinLanguage extends AbstractAntlrLanguage { - private static final String NAME = "Kotlin Parser"; + private static final String NAME = "Kotlin"; private static final String IDENTIFIER = "kotlin"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSIONS = {".kt"}; diff --git a/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java b/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java index 846a047e68..0d9ad7f150 100644 --- a/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java +++ b/languages/llvmir/src/main/java/de/jplag/llvmir/LLVMIRLanguage.java @@ -11,7 +11,7 @@ @MetaInfServices(Language.class) public class LLVMIRLanguage extends AbstractAntlrLanguage { - private static final String NAME = "LLVMIR Parser"; + private static final String NAME = "LLVM IR"; private static final String IDENTIFIER = "llvmir"; private static final int DEFAULT_MIN_TOKEN_MATCH = 70; private static final String[] FILE_EXTENSIONS = {".ll"}; diff --git a/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java b/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java index 3df6587284..2a70481276 100644 --- a/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java +++ b/languages/python-3/src/main/java/de/jplag/python3/PythonLanguage.java @@ -6,7 +6,7 @@ @MetaInfServices(de.jplag.Language.class) public class PythonLanguage extends AbstractAntlrLanguage { - + private static final String NAME = "Python"; private static final String IDENTIFIER = "python3"; public PythonLanguage() { @@ -20,7 +20,7 @@ public String[] suffixes() { @Override public String getName() { - return "Python3 Parser"; + return NAME; } @Override diff --git a/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java b/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java index d1d02cfa9c..3e7e3ab7f5 100644 --- a/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java +++ b/languages/rlang/src/main/java/de/jplag/rlang/RLanguage.java @@ -9,7 +9,7 @@ */ @MetaInfServices(de.jplag.Language.class) public class RLanguage extends AbstractAntlrLanguage { - private static final String NAME = "R Parser"; + private static final String NAME = "R"; private static final String IDENTIFIER = "rlang"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSION = {".R", ".r"}; diff --git a/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java b/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java index 50f0826e04..1a5cfa4f81 100644 --- a/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java +++ b/languages/rust/src/main/java/de/jplag/rust/RustLanguage.java @@ -16,7 +16,7 @@ public class RustLanguage implements de.jplag.Language { protected static final String[] FILE_EXTENSIONS = {".rs"}; - private static final String NAME = "Rust Language Module"; + private static final String NAME = "Rust"; private static final String IDENTIFIER = "rust"; private static final int MINIMUM_TOKEN_MATCH = 8; diff --git a/languages/scala/pom.xml b/languages/scala/pom.xml index 72861aea51..dac8a912ab 100644 --- a/languages/scala/pom.xml +++ b/languages/scala/pom.xml @@ -10,7 +10,7 @@ scala - 2.13.14 + 2.13.15 2.13 @@ -25,7 +25,7 @@ org.scalameta scalameta_${scala.compat.version} - 4.9.7 + 4.11.0 diff --git a/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala b/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala index 424b0f7334..bf226a5ced 100644 --- a/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala +++ b/languages/scala/src/main/scala/de/jplag/scala/ScalaLanguage.scala @@ -14,7 +14,7 @@ class ScalaLanguage extends de.jplag.Language { override def suffixes: Array[String] = fileExtensions - override def getName = "Scala parser" + override def getName = "Scala" override def getIdentifier = "scala" diff --git a/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java b/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java index 0ebbf4ef97..13e0c89f72 100644 --- a/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java +++ b/languages/scheme/src/main/java/de/jplag/scheme/SchemeLanguage.java @@ -12,6 +12,7 @@ @MetaInfServices(de.jplag.Language.class) public class SchemeLanguage implements de.jplag.Language { + private static final String NAME = "Scheme"; private static final String IDENTIFIER = "scheme"; private final de.jplag.scheme.Parser parser; @@ -26,7 +27,7 @@ public String[] suffixes() { @Override public String getName() { - return "SchemeR4RS Parser [basic markup]"; + return NAME; } @Override diff --git a/languages/scxml/pom.xml b/languages/scxml/pom.xml index b91e5adcd6..6e30202c70 100644 --- a/languages/scxml/pom.xml +++ b/languages/scxml/pom.xml @@ -12,7 +12,7 @@ org.assertj assertj-core - 3.26.0 + 3.26.3 test diff --git a/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java b/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java index ec6316f4dd..ba4af3e8eb 100644 --- a/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java +++ b/languages/scxml/src/main/java/de/jplag/scxml/ScxmlLanguage.java @@ -26,7 +26,7 @@ public class ScxmlLanguage implements de.jplag.Language { */ public static final String VIEW_FILE_SUFFIX = ".scxmlview"; - private static final String NAME = "SCXML (Statechart XML)"; + private static final String NAME = "SCXML"; private static final String IDENTIFIER = "scxml"; private static final int DEFAULT_MIN_TOKEN_MATCH = 6; diff --git a/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java b/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java index 87e13269fa..82a56d11e4 100644 --- a/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java +++ b/languages/swift/src/main/java/de/jplag/swift/SwiftLanguage.java @@ -17,7 +17,7 @@ public class SwiftLanguage implements de.jplag.Language { private static final String IDENTIFIER = "swift"; - private static final String NAME = "Swift Parser"; + private static final String NAME = "Swift"; private static final int DEFAULT_MIN_TOKEN_MATCH = 8; private static final String[] FILE_EXTENSIONS = {".swift"}; private final SwiftParserAdapter parserAdapter; diff --git a/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java b/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java index 5727130097..057c06e56e 100644 --- a/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java +++ b/languages/text/src/main/java/de/jplag/text/NaturalLanguage.java @@ -18,6 +18,7 @@ public class NaturalLanguage implements de.jplag.Language { private static final String IDENTIFIER = "text"; + private static final String NAME = "Text (naive)"; private final ParserAdapter parserAdapter; public NaturalLanguage() { @@ -31,7 +32,7 @@ public String[] suffixes() { @Override public String getName() { - return "Text Parser (naive)"; + return NAME; } @Override diff --git a/languages/text/src/main/java/de/jplag/text/ParserAdapter.java b/languages/text/src/main/java/de/jplag/text/ParserAdapter.java index c3d1ccc831..09f15e15b9 100644 --- a/languages/text/src/main/java/de/jplag/text/ParserAdapter.java +++ b/languages/text/src/main/java/de/jplag/text/ParserAdapter.java @@ -51,7 +51,7 @@ public List parse(Set files) throws ParsingException { private void parseFile(File file) throws ParsingException { this.currentFile = file; this.currentLine = 1; // lines start at 1 - this.currentLineBreakIndex = 0; + this.currentLineBreakIndex = -1; String content = readFile(file); int lastTokenEnd = 0; CoreDocument coreDocument = pipeline.processToCoreDocument(content); diff --git a/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java b/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java index 9fa5ad514c..8dda2b3486 100644 --- a/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java +++ b/languages/typescript/src/main/java/de/jplag/typescript/TypeScriptLanguage.java @@ -11,6 +11,7 @@ public class TypeScriptLanguage extends AbstractAntlrLanguage { private static final String IDENTIFIER = "typescript"; + private static final String NAME = "TypeScript"; private final TypeScriptLanguageOptions options = new TypeScriptLanguageOptions(); @Override @@ -20,7 +21,7 @@ public String[] suffixes() { @Override public String getName() { - return "Typescript Parser"; + return NAME; } @Override diff --git a/pom.xml b/pom.xml index 1b8f58ecb5..de038e5a61 100644 --- a/pom.xml +++ b/pom.xml @@ -75,15 +75,15 @@ 21 21 2.43.0 - 2.0.13 - 5.10.3 + 2.0.16 + 5.11.3 2.7.7 - 4.13.1 - 2.36.0 - 2.30.0 - 2.37.0 - 3.20.200 + 4.13.2 + 2.37.0 + 2.31.0 + 2.38.0 + 3.21.0 1.1.0 @@ -141,7 +141,7 @@ com.fasterxml.jackson.core jackson-databind - 2.17.1 + 2.18.0 @@ -168,7 +168,7 @@ org.mockito mockito-core - 5.12.0 + 5.14.2 test @@ -240,7 +240,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.3.0 + 3.5.1 org.jacoco @@ -269,12 +269,12 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.4 + 3.2.7 org.apache.maven.plugins maven-deploy-plugin - 3.1.2 + 3.1.3 @@ -331,7 +331,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.7.0 + 3.10.1 attach-javadocs diff --git a/report-viewer/.eslintrc.cjs b/report-viewer/.eslintrc.cjs index dd9e51bf7f..894ad63d50 100644 --- a/report-viewer/.eslintrc.cjs +++ b/report-viewer/.eslintrc.cjs @@ -14,7 +14,7 @@ module.exports = { }, plugins: ['@typescript-eslint', 'vue'], rules: { - 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], 'no-restricted-exports': ['error', { restrictDefaultExports: { direct: true } }], 'vue/no-setup-props-reactivity-loss': 'error' }, diff --git a/report-viewer/.husky/pre-commit b/report-viewer/.husky/pre-commit deleted file mode 100644 index b4eb63f65d..0000000000 --- a/report-viewer/.husky/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -cd ./report-viewer -npx lint-staged \ No newline at end of file diff --git a/report-viewer/README.md b/report-viewer/README.md index c39bf4f8ef..f1d633c300 100644 --- a/report-viewer/README.md +++ b/report-viewer/README.md @@ -1,37 +1,52 @@ # JPlag Report Viewer -The JPlag Report Viewer is a Vue 3 + Typescript standalone application that can be used to display the JSON files generated by the JPlag reporting. The application requires Node.js and npm to be installed on the system. +The JPlag Report Viewer is a web application that can be used to display the zip file generated by JPlag. -Before the first run execute: - -- Install necessary dependencies by running `npm install` in the /report-viewer folder. -- Start the application by running the `npm run dev` command in the /report-viewer folder. -- The report viewer is now accessible in your browser under http://localhost:8080/ ## Project setup +The application requires Node.js and npm to be installed on the system. ``` npm install ``` -### Compiles and hot-reloads for development +### Run the development server ``` npm run dev ``` -### Compiles and minifies for production +### Compile and build +There are different ways to build the report-viewer. + +The report viewer will be built and packaged with the cli in a jar file if built with the `with-report-viewer` profile: +``` +mvn -Pwith-report-viewer clean package assembly:single +``` + +To build it in the standard way, without any base url, run: ``` npm run build ``` -### Lints and fixes files +For production builds (for example to host on GitHub Pages in a repository called `JPlag`) run: ``` -npm run lint +npm run build:prod ``` +When hosting this build it will need to be accessible under `yourdomain.tld/JPlag/`. -### Format files with prettier +To build the demo version run: ``` -npm run format +npm run build:demo ``` +Similar to the production build, this build will have `demo` as its base url. -### Customize configuration -See [Configuration Reference](https://cli.vuejs.org/config/). + +## Contributing + +We're happy to incorporate all improvements to JPlag into this codebase. Feel free to fork the project and send pull requests. Please consider our guidelines for contributions. + +Before committing please run the following commands to ensure that the code is properly formatted and linted. +``` +npm run format +npm run lint +``` +This can also be done automatically by the precommit hooks. They get automatically installed when running `npm install`. diff --git a/report-viewer/package-lock.json b/report-viewer/package-lock.json index a1bce2a994..30c9a435d6 100644 --- a/report-viewer/package-lock.json +++ b/report-viewer/package-lock.json @@ -8,48 +8,48 @@ "name": "report-viewer", "version": "0.0.0", "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.2", - "@fortawesome/free-brands-svg-icons": "^6.5.2", - "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/vue-fontawesome": "^3.0.8", - "chart.js": "^4.4.3", - "chartjs-chart-graph": "^4.3.1", + "chart.js": "^4.4.5", + "chartjs-chart-graph": "^4.3.3", "chartjs-plugin-datalabels": "^2.2.0", - "highlight.js": "^11.9.0", + "highlight.js": "^11.10.0", "jszip": "^3.10.0", - "pinia": "^2.1.7", + "pinia": "^2.2.4", "slash": "^5.1.0", - "vue": "^3.4.31", + "vue": "^3.5.12", "vue-chartjs": "^5.3.1", "vue-draggable-next": "^2.2.1", - "vue-router": "^4.4.0", + "vue-router": "^4.4.5", "vue-virtual-scroller": "^2.0.0-beta.8" }, "devDependencies": { - "@playwright/test": "^1.45.1", - "@rushstack/eslint-patch": "^1.10.3", + "@pinia/testing": "^0.1.6", + "@playwright/test": "^1.48.0", + "@rushstack/eslint-patch": "^1.10.4", "@types/jsdom": "^21.1.7", - "@types/node": "^18.19.39", - "@vitejs/plugin-vue": "^5.0.5", - "@vue/eslint-config-prettier": "^9.0.0", + "@types/node": "^22.7.9", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-typescript": "^13.0.0", "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.5.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.26.0", - "husky": "^9.0.11", - "jsdom": "^24.1.0", - "lint-staged": "^15.2.7", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", + "eslint-plugin-vue": "^9.29.1", + "jsdom": "^25.0.1", + "lint-staged": "^15.2.10", "npm-run-all": "^4.1.5", - "postcss": "^8.4.35", - "prettier": "^3.3.2", - "prettier-plugin-tailwindcss": "^0.6.5", - "tailwindcss": "^3.4.4", - "typescript": "^5.5.3", - "vite": "^5.3.1", - "vitest": "^1.6.0", - "vue-tsc": "^2.0.21" + "postcss": "^8.4.45", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", + "tailwindcss": "^3.4.13", + "typescript": "^5.5.4", + "vite": "^5.4.10", + "vitest": "^2.1.2", + "vue-tsc": "^2.1.6" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -73,10 +73,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dependencies": { + "@babel/types": "^7.25.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -84,6 +103,19 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -522,77 +554,55 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", - "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", - "hasInstallScript": true, - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" - }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", "engines": { "node": ">=6" } }, - "node_modules/@fortawesome/fontawesome-svg-core/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", - "hasInstallScript": true, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.5.2.tgz", - "integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==", - "hasInstallScript": true, + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" } }, - "node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", - "hasInstallScript": true, - "engines": { - "node": ">=6" - } - }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", - "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", - "hasInstallScript": true, + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", "dependencies": { - "@fortawesome/fontawesome-common-types": "6.5.2" + "@fortawesome/fontawesome-common-types": "6.6.0" }, "engines": { "node": ">=6" } }, - "node_modules/@fortawesome/free-solid-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", - "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", - "hasInstallScript": true, - "engines": { - "node": ">=6" - } - }, "node_modules/@fortawesome/vue-fontawesome": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz", @@ -603,12 +613,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -652,9 +663,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@isaacs/cliui": { @@ -701,59 +713,47 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -806,6 +806,47 @@ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", "dev": true }, + "node_modules/@pinia/testing": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-0.1.6.tgz", + "integrity": "sha512-Q40s3kpjXpjmcnc61l84wyG83yVmkBi5rRdSoPpwQoRfSnNKKr52XjFFt6hP8iBxehYS9NR+D57T1uzgnEVPHg==", + "dev": true, + "dependencies": { + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "pinia": ">=2.2.3" + } + }, + "node_modules/@pinia/testing/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -829,12 +870,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", - "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz", + "integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==", "dev": true, "dependencies": { - "playwright": "1.45.1" + "playwright": "1.48.0" }, "bin": { "playwright": "cli.js" @@ -844,9 +885,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.2.tgz", - "integrity": "sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], @@ -857,9 +898,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.2.tgz", - "integrity": "sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], @@ -870,9 +911,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.2.tgz", - "integrity": "sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], @@ -883,9 +924,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.2.tgz", - "integrity": "sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], @@ -896,9 +937,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.2.tgz", - "integrity": "sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", "cpu": [ "arm" ], @@ -909,9 +963,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.2.tgz", - "integrity": "sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], @@ -922,9 +976,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.2.tgz", - "integrity": "sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], @@ -935,11 +989,11 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.13.2.tgz", - "integrity": "sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", "cpu": [ - "ppc64le" + "ppc64" ], "dev": true, "optional": true, @@ -948,9 +1002,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.2.tgz", - "integrity": "sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], @@ -961,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.13.2.tgz", - "integrity": "sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", "cpu": [ "s390x" ], @@ -974,9 +1028,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.2.tgz", - "integrity": "sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], @@ -987,9 +1041,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.2.tgz", - "integrity": "sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], @@ -1000,9 +1054,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.2.tgz", - "integrity": "sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], @@ -1013,9 +1067,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.2.tgz", - "integrity": "sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], @@ -1026,9 +1080,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.2.tgz", - "integrity": "sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], @@ -1039,26 +1093,20 @@ ] }, "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", - "dev": true - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", "dev": true }, "node_modules/@types/d3-force": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", - "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" }, "node_modules/@types/d3-hierarchy": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.6.tgz", - "integrity": "sha512-qlmD/8aMk5xGorUvTUWHCiumvgaUXYldYjNVOWtYoTYY/L+WwIEAmJxUmTgr9LoGNG0PPAOmqMDJVDPc7DOpPw==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" }, "node_modules/@types/estree": { "version": "1.0.5", @@ -1084,12 +1132,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", - "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", + "version": "22.7.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz", + "integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/semver": { @@ -1301,9 +1349,9 @@ "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz", - "integrity": "sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -1314,137 +1362,145 @@ } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz", + "integrity": "sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.1.2", + "@vitest/utils": "2.1.2", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "node_modules/@vitest/mocker": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.2.tgz", + "integrity": "sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.2", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "node_modules/@vitest/pretty-format": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz", + "integrity": "sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==", "dev": true, "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" + "tinyrainbow": "^1.2.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "node_modules/@vitest/runner": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.2.tgz", + "integrity": "sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==", "dev": true, - "engines": { - "node": ">=12.20" + "dependencies": { + "@vitest/utils": "2.1.2", + "pathe": "^1.1.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.2.tgz", + "integrity": "sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.1.2", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.2.tgz", + "integrity": "sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz", + "integrity": "sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.1.2", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@volar/language-core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.3.0.tgz", - "integrity": "sha512-pvhL24WUh3VDnv7Yw5N1sjhPtdx7q9g+Wl3tggmnkMcyK8GcCNElF2zHiKznryn0DiUGk+eez/p2qQhz+puuHw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.1.tgz", + "integrity": "sha512-9AKhC7Qn2mQYxj7Dz3bVxeOk7gGJladhWixUYKef/o0o7Bm4an+A3XvmcTHVqZ8stE6lBVH++g050tBtJ4TZPQ==", "dev": true, "dependencies": { - "@volar/source-map": "2.3.0" + "@volar/source-map": "2.4.1" } }, "node_modules/@volar/source-map": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.3.0.tgz", - "integrity": "sha512-G/228aZjAOGhDjhlyZ++nDbKrS9uk+5DMaEstjvzglaAw7nqtDyhnQAsYzUg6BMP9BtwZ59RIw5HGePrutn00Q==", - "dev": true, - "dependencies": { - "muggle-string": "^0.4.0" - } + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.1.tgz", + "integrity": "sha512-Xq6ep3OZg9xUqN90jEgB9ztX5SsTz1yiV8wiQbcYNjWkek+Ie3dc8l7AVt3EhDm9mSIR58oWczHkzM2H6HIsmQ==", + "dev": true }, "node_modules/@volar/typescript": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.3.0.tgz", - "integrity": "sha512-PtUwMM87WsKVeLJN33GSTUjBexlKfKgouWlOUIv7pjrOnTwhXHZNSmpc312xgXdTjQPpToK6KXSIcKu9sBQ5LQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.1.tgz", + "integrity": "sha512-UoRzC0PXcwajFQTu8XxKSYNsWNBtVja6Y9gC8eLv7kYm+UEKJCcZ8g7dialsOYA0HKs3Vpg57MeCsawFLC6m9Q==", "dev": true, "dependencies": { - "@volar/language-core": "2.3.0", + "@volar/language-core": "2.4.1", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz", - "integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/shared": "3.4.31", + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" @@ -1456,27 +1512,27 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-dom": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz", - "integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", "dependencies": { - "@vue/compiler-core": "3.4.31", - "@vue/shared": "3.4.31" + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz", - "integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==", - "dependencies": { - "@babel/parser": "^7.24.7", - "@vue/compiler-core": "3.4.31", - "@vue/compiler-dom": "3.4.31", - "@vue/compiler-ssr": "3.4.31", - "@vue/shared": "3.4.31", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", "source-map-js": "^1.2.0" } }, @@ -1486,30 +1542,40 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz", - "integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", "dependencies": { - "@vue/compiler-dom": "3.4.31", - "@vue/shared": "3.4.31" + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" } }, "node_modules/@vue/devtools-api": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz", - "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==" + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" }, "node_modules/@vue/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.1.0.tgz", + "integrity": "sha512-J6wV91y2pXc0Phha01k0WOHBTPsoSTf4xlmMjoKaeSxBpAdsgTppGF5RZRdOHM7OA74zAXD+VLANrtYXpiPKkQ==", "dev": true, "dependencies": { - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0" + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1" }, "peerDependencies": { - "eslint": ">= 8.0.0", + "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, @@ -1538,18 +1604,19 @@ } }, "node_modules/@vue/language-core": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.21.tgz", - "integrity": "sha512-vjs6KwnCK++kIXT+eI63BGpJHfHNVJcUCr3RnvJsccT3vbJnZV5IhHR2puEkoOkIbDdp0Gqi1wEnv3hEd3WsxQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", "dev": true, "dependencies": { - "@volar/language-core": "~2.3.0-alpha.15", + "@volar/language-core": "~2.4.1", "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", - "path-browserify": "^1.0.1", - "vue-template-compiler": "^2.7.14" + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" @@ -1561,49 +1628,49 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz", - "integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", "dependencies": { - "@vue/shared": "3.4.31" + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz", - "integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", "dependencies": { - "@vue/reactivity": "3.4.31", - "@vue/shared": "3.4.31" + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz", - "integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", "dependencies": { - "@vue/reactivity": "3.4.31", - "@vue/runtime-core": "3.4.31", - "@vue/shared": "3.4.31", + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz", - "integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", "dependencies": { - "@vue/compiler-ssr": "3.4.31", - "@vue/shared": "3.4.31" + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { - "vue": "3.4.31" + "vue": "3.5.12" } }, "node_modules/@vue/shared": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz", - "integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==" + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==" }, "node_modules/@vue/test-utils": { "version": "2.4.6", @@ -1651,15 +1718,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -1689,12 +1747,15 @@ } }, "node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1799,12 +1860,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/asynckit": { @@ -1814,9 +1875,9 @@ "dev": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -1833,11 +1894,11 @@ } ], "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -1905,9 +1966,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "dev": true, "funding": [ { @@ -1924,10 +1985,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -1978,9 +2039,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001605", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz", - "integrity": "sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { @@ -1998,21 +2059,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -2032,9 +2091,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", - "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", + "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -2043,12 +2102,12 @@ } }, "node_modules/chartjs-chart-graph": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/chartjs-chart-graph/-/chartjs-chart-graph-4.3.1.tgz", - "integrity": "sha512-dZQcR+rYxg0zDG229Um59e/4MHoOqO2CYcY2VE5juTB+HYxycObxs1Kdgk5FvaWmiMSE7oNG9VkOLxh9n0oTvw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/chartjs-chart-graph/-/chartjs-chart-graph-4.3.3.tgz", + "integrity": "sha512-34xE1bvZNEkIUYzfx06LQa+CAGNUoz3c+ihrMHh1/XUrBQga4MSDD7cMhCjWPfeLj+TPMu2EbN3WbWne63JDuA==", "dependencies": { - "@types/d3-force": "^3.0.9", - "@types/d3-hierarchy": "^3.1.6", + "@types/d3-force": "^3.0.10", + "@types/d3-hierarchy": "^3.1.7", "d3-dispatch": "^3.0.1", "d3-force": "^3.0.0", "d3-hierarchy": "^3.1.2", @@ -2068,15 +2127,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -2119,15 +2175,15 @@ } }, "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "dependencies": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2168,9 +2224,9 @@ "dev": true }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "dependencies": { "emoji-regex": "^10.3.0", @@ -2298,12 +2354,12 @@ } }, "node_modules/cssstyle": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", - "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", "dev": true, "dependencies": { - "rrweb-cssom": "^0.6.0" + "rrweb-cssom": "^0.7.1" }, "engines": { "node": ">=18" @@ -2379,9 +2435,9 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -2402,13 +2458,10 @@ "dev": true }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -2465,15 +2518,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2544,9 +2588,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.693", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.693.tgz", - "integrity": "sha512-/if4Ueg0GUQlhCrW2ZlXwDAm40ipuKo+OgeHInlL8sbjt+hzISxZK949fZeJaVsheamrzANXvw1zQTvbxTvSHw==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.7.tgz", + "integrity": "sha512-6FTNWIWMxMy/ZY6799nBlPtF1DFDQ6VQJ7yyDP27SJNt5lwtQ5ufqVvHylb3fdQefvRcgA3fKcFMJi9OLwBRNw==", "dev": true }, "node_modules/emoji-regex": { @@ -2566,6 +2610,18 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2719,16 +2775,16 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -2786,13 +2842,13 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2816,9 +2872,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.26.0.tgz", - "integrity": "sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==", + "version": "9.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.1.tgz", + "integrity": "sha512-MH/MbVae4HV/tM8gKAVWMPJbYgW04CK7SuzYRrlNERpxbO0P3+Zdsa2oAcFBW6xNu7W6lIkGOsFAMCRTYmrlWQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -2826,8 +2882,8 @@ "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", - "semver": "^7.6.0", - "vue-eslint-parser": "^9.4.2", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", "xml-name-validator": "^4.0.0" }, "engines": { @@ -3225,15 +3281,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -3485,9 +3532,9 @@ } }, "node_modules/highlight.js": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", - "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz", + "integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==", "engines": { "node": ">=12.0.0" } @@ -3524,9 +3571,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -3545,21 +3592,6 @@ "node": ">=16.17.0" } }, - "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", - "dev": true, - "bin": { - "husky": "bin.mjs" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3984,12 +4016,6 @@ "node": ">=14" } }, - "node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4003,31 +4029,31 @@ } }, "node_modules/jsdom": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", - "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "dependencies": { - "cssstyle": "^4.0.1", + "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.10", + "nwsapi": "^2.2.12", "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.0", + "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.4", + "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", - "ws": "^8.17.0", + "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "engines": { @@ -4042,12 +4068,6 @@ } } }, - "node_modules/jsdom/node_modules/rrweb-cssom": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.0.tgz", - "integrity": "sha512-KlSv0pm9kgQSRxXEMgtivPJ4h826YHsuob8pSHcfSZsSXGtvpEAie8S0AnXuObEJ7nhikOb4ahwxDm0H2yW17g==", - "dev": true - }, "node_modules/jsdom/node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", @@ -4081,12 +4101,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4129,9 +4143,9 @@ } }, "node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, "engines": { "node": ">=14" @@ -4147,21 +4161,21 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.2.7", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.7.tgz", - "integrity": "sha512-+FdVbbCZ+yoh7E/RosSdqKJyUM2OEjTciH0TFNkawKgvFp1zbGlEC39RADg+xKBG1R4mhoH2j85myBQZ5wR+lw==", + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", "dev": true, "dependencies": { "chalk": "~5.3.0", "commander": "~12.1.0", - "debug": "~4.3.4", + "debug": "~4.3.6", "execa": "~8.0.1", - "lilconfig": "~3.1.1", - "listr2": "~8.2.1", - "micromatch": "~4.0.7", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", - "yaml": "~2.4.2" + "yaml": "~2.5.0" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -4195,16 +4209,16 @@ } }, "node_modules/listr2": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.1.tgz", - "integrity": "sha512-irTfvpib/rNiD637xeevjO2l3Z5loZmuaRi0L0YE5LfijwVY96oyVn0DFD3o/teAok7nfobMG1THvvcHh/BP6g==", + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", "dev": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", - "log-update": "^6.0.0", - "rfdc": "^1.3.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" }, "engines": { @@ -4242,9 +4256,9 @@ "dev": true }, "node_modules/listr2/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "dependencies": { "emoji-regex": "^10.3.0", @@ -4305,22 +4319,6 @@ "node": ">=4" } }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4349,14 +4347,14 @@ "dev": true }, "node_modules/log-update": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", - "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "dependencies": { - "ansi-escapes": "^6.2.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^7.0.0", + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" }, @@ -4429,9 +4427,9 @@ } }, "node_modules/log-update/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "dependencies": { "emoji-regex": "^10.3.0", @@ -4478,13 +4476,10 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true }, "node_modules/lru-cache": { "version": "10.2.0", @@ -4496,11 +4491,11 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/memorystream": { @@ -4528,9 +4523,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -4573,6 +4568,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -4602,18 +4609,6 @@ "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==" }, - "node_modules/mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4667,9 +4662,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/nopt": { @@ -4963,9 +4958,9 @@ } }, "node_modules/nwsapi": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==", + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", "dev": true }, "node_modules/object-assign": { @@ -5206,18 +5201,18 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5253,12 +5248,12 @@ } }, "node_modules/pinia": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", - "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.4.tgz", + "integrity": "sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==", "dependencies": { - "@vue/devtools-api": "^6.5.0", - "vue-demi": ">=0.14.5" + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" }, "funding": { "url": "https://github.com/sponsors/posva" @@ -5278,9 +5273,9 @@ } }, "node_modules/pinia/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", @@ -5311,24 +5306,13 @@ "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/playwright": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", - "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz", + "integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==", "dev": true, "dependencies": { - "playwright-core": "1.45.1" + "playwright-core": "1.48.0" }, "bin": { "playwright": "cli.js" @@ -5341,9 +5325,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", - "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz", + "integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -5353,9 +5337,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -5372,8 +5356,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5498,9 +5482,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -5525,9 +5509,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz", - "integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz", + "integrity": "sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==", "dev": true, "engines": { "node": ">=14.21.3" @@ -5544,6 +5528,7 @@ "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", @@ -5581,6 +5566,9 @@ "prettier-plugin-marko": { "optional": true }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, "prettier-plugin-organize-attributes": { "optional": true }, @@ -5598,32 +5586,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5635,12 +5597,6 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5650,12 +5606,6 @@ "node": ">=6" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5676,12 +5626,6 @@ } ] }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5769,12 +5713,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5802,51 +5740,36 @@ } }, "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5858,9 +5781,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, "node_modules/rimraf": { @@ -5921,9 +5844,9 @@ } }, "node_modules/rollup": { - "version": "4.13.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.2.tgz", - "integrity": "sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -5936,28 +5859,29 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.2", - "@rollup/rollup-android-arm64": "4.13.2", - "@rollup/rollup-darwin-arm64": "4.13.2", - "@rollup/rollup-darwin-x64": "4.13.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.2", - "@rollup/rollup-linux-arm64-gnu": "4.13.2", - "@rollup/rollup-linux-arm64-musl": "4.13.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.13.2", - "@rollup/rollup-linux-riscv64-gnu": "4.13.2", - "@rollup/rollup-linux-s390x-gnu": "4.13.2", - "@rollup/rollup-linux-x64-gnu": "4.13.2", - "@rollup/rollup-linux-x64-musl": "4.13.2", - "@rollup/rollup-win32-arm64-msvc": "4.13.2", - "@rollup/rollup-win32-ia32-msvc": "4.13.2", - "@rollup/rollup-win32-x64-msvc": "4.13.2", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", "dev": true }, "node_modules/run-parallel": { @@ -6048,13 +5972,10 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -6062,18 +5983,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/set-function-length": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", @@ -6217,9 +6126,9 @@ "peer": true }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -6479,18 +6388,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -6553,9 +6450,9 @@ "dev": true }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -6569,9 +6466,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -6642,29 +6539,70 @@ } }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "engines": { "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.52.tgz", + "integrity": "sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.52" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.52.tgz", + "integrity": "sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6678,18 +6616,15 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", "dev": true, "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" + "node": ">=16" } }, "node_modules/tr46": { @@ -6723,9 +6658,9 @@ "dev": true }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true }, "node_modules/type-check": { @@ -6740,15 +6675,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6827,9 +6753,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -6839,12 +6765,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -6861,24 +6781,15 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -6895,8 +6806,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6914,16 +6825,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6940,14 +6841,14 @@ } }, "node_modules/vite": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", - "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", + "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", "dev": true, "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -6966,6 +6867,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -6983,6 +6885,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -6995,15 +6900,14 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz", + "integrity": "sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.6", + "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { @@ -7031,31 +6935,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.2.tgz", + "integrity": "sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.2", + "@vitest/mocker": "2.1.2", + "@vitest/pretty-format": "^2.1.2", + "@vitest/runner": "2.1.2", + "@vitest/snapshot": "2.1.2", + "@vitest/spy": "2.1.2", + "@vitest/utils": "2.1.2", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.1.2", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -7069,8 +6972,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.1.2", + "@vitest/ui": "2.1.2", "happy-dom": "*", "jsdom": "*" }, @@ -7102,15 +7005,15 @@ "dev": true }, "node_modules/vue": { - "version": "3.4.31", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz", - "integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==", + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", "dependencies": { - "@vue/compiler-dom": "3.4.31", - "@vue/compiler-sfc": "3.4.31", - "@vue/runtime-dom": "3.4.31", - "@vue/server-renderer": "3.4.31", - "@vue/shared": "3.4.31" + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" }, "peerDependencies": { "typescript": "*" @@ -7146,9 +7049,9 @@ } }, "node_modules/vue-eslint-parser": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", - "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", "dev": true, "dependencies": { "debug": "^4.3.4", @@ -7186,11 +7089,11 @@ } }, "node_modules/vue-router": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", - "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz", + "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==", "dependencies": { - "@vue/devtools-api": "^6.5.1" + "@vue/devtools-api": "^6.6.4" }, "funding": { "url": "https://github.com/sponsors/posva" @@ -7199,31 +7102,21 @@ "vue": "^3.2.0" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dev": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, "node_modules/vue-tsc": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.21.tgz", - "integrity": "sha512-E6x1p1HaHES6Doy8pqtm7kQern79zRtIewkf9fiv7Y43Zo4AFDS5hKi+iHi2RwEhqRmuiwliB1LCEFEGwvxQnw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz", + "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==", "dev": true, "dependencies": { - "@volar/typescript": "~2.3.0-alpha.15", - "@vue/language-core": "2.0.21", + "@volar/typescript": "~2.4.1", + "@vue/language-core": "2.1.6", "semver": "^7.5.4" }, "bin": { "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "typescript": "*" + "typescript": ">=5.0.0" } }, "node_modules/vue-virtual-scroller": { @@ -7354,9 +7247,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -7479,9 +7372,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { "node": ">=10.0.0" @@ -7514,16 +7407,10 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "dev": true, "bin": { "yaml": "bin.mjs" diff --git a/report-viewer/package.json b/report-viewer/package.json index 132c370b83..5119544289 100644 --- a/report-viewer/package.json +++ b/report-viewer/package.json @@ -16,50 +16,50 @@ "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore --max-warnings 0", "format": "prettier --write src/", - "prepare": "cd .. && husky report-viewer/.husky" + "prepare": "git config --local core.hooksPath gitHooks/hooks" }, "dependencies": { - "@fortawesome/fontawesome-svg-core": "^6.5.2", - "@fortawesome/free-brands-svg-icons": "^6.5.2", - "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/vue-fontawesome": "^3.0.8", - "chart.js": "^4.4.3", - "chartjs-chart-graph": "^4.3.1", + "chart.js": "^4.4.5", + "chartjs-chart-graph": "^4.3.3", "chartjs-plugin-datalabels": "^2.2.0", - "highlight.js": "^11.9.0", + "highlight.js": "^11.10.0", "jszip": "^3.10.0", - "pinia": "^2.1.7", + "pinia": "^2.2.4", "slash": "^5.1.0", - "vue": "^3.4.31", + "vue": "^3.5.12", "vue-chartjs": "^5.3.1", "vue-draggable-next": "^2.2.1", - "vue-router": "^4.4.0", + "vue-router": "^4.4.5", "vue-virtual-scroller": "^2.0.0-beta.8" }, "devDependencies": { - "@playwright/test": "^1.45.1", - "@rushstack/eslint-patch": "^1.10.3", + "@pinia/testing": "^0.1.6", + "@playwright/test": "^1.48.0", + "@rushstack/eslint-patch": "^1.10.4", "@types/jsdom": "^21.1.7", - "@types/node": "^18.19.39", - "@vitejs/plugin-vue": "^5.0.5", - "@vue/eslint-config-prettier": "^9.0.0", + "@types/node": "^22.7.9", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/eslint-config-prettier": "^10.1.0", "@vue/eslint-config-typescript": "^13.0.0", "@vue/test-utils": "^2.4.6", "@vue/tsconfig": "^0.5.1", - "autoprefixer": "^10.4.19", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.26.0", - "husky": "^9.0.11", - "jsdom": "^24.1.0", - "lint-staged": "^15.2.7", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.1", + "eslint-plugin-vue": "^9.29.1", + "jsdom": "^25.0.1", + "lint-staged": "^15.2.10", "npm-run-all": "^4.1.5", - "postcss": "^8.4.35", - "prettier": "^3.3.2", - "prettier-plugin-tailwindcss": "^0.6.5", - "tailwindcss": "^3.4.4", - "typescript": "^5.5.3", - "vite": "^5.3.1", - "vitest": "^1.6.0", - "vue-tsc": "^2.0.21" + "postcss": "^8.4.45", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", + "tailwindcss": "^3.4.13", + "typescript": "^5.5.4", + "vite": "^5.4.10", + "vitest": "^2.1.2", + "vue-tsc": "^2.1.6" } } diff --git a/report-viewer/src/App.vue b/report-viewer/src/App.vue index c5b5da48c6..58b62b9de5 100644 --- a/report-viewer/src/App.vue +++ b/report-viewer/src/App.vue @@ -1,7 +1,7 @@ @@ -25,7 +34,47 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { library } from '@fortawesome/fontawesome-svg-core' import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons' import { store } from './stores/store' +import ToastComponent from './components/ToastComponent.vue' +import { Version, reportViewerVersion } from './model/Version' +import { computed, ref } from 'vue' library.add(faMoon) library.add(faSun) + +const newestVersion = ref(new Version(-1, -1, -1)) +const isDemo = import.meta.env.MODE == 'demo' +const hasShownToast = ref(sessionStorage.getItem('hasShownToast') == 'true') + +const showToast = computed(() => { + const value = + !isDemo && + !newestVersion.value.isInvalid() && + !reportViewerVersion.isDevVersion() && + newestVersion.value.compareTo(reportViewerVersion) > 0 && + !hasShownToast.value + + if (value) { + sessionStorage.setItem('hasShownToast', 'true') + } else { + sessionStorage.removeItem('hasShownToast') + } + + return value +}) + +fetch('https://api.github.com/repos/jplag/JPlag/releases/latest') + .then((response) => response.json()) + .then((data) => { + const versionString = data.tag_name + // remove the 'v' from the version string and split it into an array + const versionArray = versionString.substring(1).split('.') + newestVersion.value = new Version( + parseInt(versionArray[0]), + parseInt(versionArray[1]), + parseInt(versionArray[2]) + ) + }) + .catch(() => { + newestVersion.value = new Version(-1, -1, -1) + }) diff --git a/report-viewer/src/assets/jplag-dark-transparent.png b/report-viewer/src/assets/jplag-dark-transparent.png index 6ce965f401..fe0bb58fec 100644 Binary files a/report-viewer/src/assets/jplag-dark-transparent.png and b/report-viewer/src/assets/jplag-dark-transparent.png differ diff --git a/report-viewer/src/assets/jplag-light-transparent.png b/report-viewer/src/assets/jplag-light-transparent.png index 732b433f21..78b171cf68 100644 Binary files a/report-viewer/src/assets/jplag-light-transparent.png and b/report-viewer/src/assets/jplag-light-transparent.png differ diff --git a/report-viewer/src/components/ClusterGraph.vue b/report-viewer/src/components/ClusterGraph.vue index b20d02b674..f2dd4d6c8d 100644 --- a/report-viewer/src/components/ClusterGraph.vue +++ b/report-viewer/src/components/ClusterGraph.vue @@ -83,7 +83,7 @@ const hoverableEdges = computed(() => { const firstIndex = keys.value.indexOf(key) const secondIndex = keys.value.indexOf(match.matchedWith) if (firstIndex == -1 || secondIndex == -1) { - console.log(`Could not find index for ${key} or ${match.matchedWith}`) + console.warn(`Could not find index for ${key} or ${match.matchedWith}`) } if (firstIndex < secondIndex) { edges.push({ @@ -165,17 +165,20 @@ const maximumSimilarity = computed(() => { return maximumSimilarity }) -function getClampedSimilarityFromKeyIndex(firstIndex: number, secondIndex: number) { +function getClampedSimilarityFromKeyIndex( + firstIndex: number, + secondIndex: number, + min: number, + max: number +) { const similarity = getSimilarityFromKeyIndex(firstIndex, secondIndex) if (similarity == 0) { return 0 } - if (minimumSimilarity.value == maximumSimilarity.value) { + if (min == max) { return 1 } - return ( - (similarity - minimumSimilarity.value) / (maximumSimilarity.value - minimumSimilarity.value) - ) + return (similarity - min) / (max - min) } function getEdgeAlphaFromKeyIndex(firstIndex: number, secondIndex: number) { @@ -183,7 +186,16 @@ function getEdgeAlphaFromKeyIndex(firstIndex: number, secondIndex: number) { if (similarity == 0) { return 1 } - return getClampedSimilarityFromKeyIndex(firstIndex, secondIndex) * 0.7 + 0.3 + return ( + getClampedSimilarityFromKeyIndex( + firstIndex, + secondIndex, + Math.min(minimumSimilarity.value, 0.5), + maximumSimilarity.value + ) * + 0.7 + + 0.3 + ) } function getEdgeWidth(firstIndex: number, secondIndex: number) { @@ -191,7 +203,7 @@ function getEdgeWidth(firstIndex: number, secondIndex: number) { if (similarity == 0) { return 0.5 } - return getClampedSimilarityFromKeyIndex(firstIndex, secondIndex) * 5 + 1 + return getClampedSimilarityFromKeyIndex(firstIndex, secondIndex, 0, 1) * 5 + 1 } function getEdgeDashStyle(firstIndex: number, secondIndex: number) { diff --git a/report-viewer/src/components/ClusterRadarChart.vue b/report-viewer/src/components/ClusterRadarChart.vue index 22b2ae01e5..f3ea8df03d 100644 --- a/report-viewer/src/components/ClusterRadarChart.vue +++ b/report-viewer/src/components/ClusterRadarChart.vue @@ -132,7 +132,7 @@ const radarChartOptions = computed(() => { } }) -const chartData: Ref> = computed(() => { +const chartData: Ref> = computed(() => { return { labels: labels.value, datasets: [ diff --git a/report-viewer/src/components/ComparisonTableFilter.vue b/report-viewer/src/components/ComparisonTableFilter.vue index 99a381b75e..b2e47a29b7 100644 --- a/report-viewer/src/components/ComparisonTableFilter.vue +++ b/report-viewer/src/components/ComparisonTableFilter.vue @@ -83,7 +83,7 @@ const searchStringValue = computed({ } for (const submissionId of store().getSubmissionIds) { - const submissionParts = submissionId.toLowerCase().split(/ +/g) + const submissionParts = store().submissionDisplayName(submissionId).toLowerCase().split(/ +/g) if (submissionParts.every((part) => searchParts.includes(part))) { store().state.anonymous.delete(submissionId) } diff --git a/report-viewer/src/components/ComparisonsTable.vue b/report-viewer/src/components/ComparisonsTable.vue index 8e345ef30d..d1856c7708 100644 --- a/report-viewer/src/components/ComparisonsTable.vue +++ b/report-viewer/src/components/ComparisonsTable.vue @@ -173,7 +173,7 @@ import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { library } from '@fortawesome/fontawesome-svg-core' import { faUserGroup } from '@fortawesome/free-solid-svg-icons' -import { generateColors } from '@/utils/ColorUtils' +import { generateHues } from '@/utils/ColorUtils' import ToolTipComponent from './ToolTipComponent.vue' import { MetricType, metricToolTips } from '@/model/MetricType' import NameElement from './NameElement.vue' @@ -213,10 +213,9 @@ const searchString = ref('') /** * This function gets called when the search bar for the comparison table has been updated. - * It updates the displayed comparisons to only show the ones that have part of any search result in their id. The search is not case sensitive. The parts can be separated by commas or spaces. - * It also updates the anonymous set to unhide a submission if its name was typed in the search bar at any point in time. + * It returns the input list, with the filter given in searchString applied. * - * @param newVal The new value of the search bar + * @param comparisons Sorted list of comparisons */ function getFilteredComparisons(comparisons: ComparisonListElement[]) { const searches = searchString.value @@ -237,9 +236,9 @@ function getFilteredComparisons(comparisons: ComparisonListElement[]) { return comparisons.filter((c) => { // name search - const id1 = c.firstSubmissionId.toLowerCase() - const id2 = c.secondSubmissionId.toLowerCase() - if (searches.some((s) => id1.includes(s) || id2.includes(s))) { + const name1 = store().submissionDisplayName(c.firstSubmissionId).toLowerCase() + const name2 = store().submissionDisplayName(c.secondSubmissionId).toLowerCase() + if (searches.some((s) => name1.includes(s) || name2.includes(s))) { return true } @@ -257,7 +256,7 @@ function getFilteredComparisons(comparisons: ComparisonListElement[]) { [MetricType.MAXIMUM]: [] } metricSearches.forEach((s) => { - const regexResult = /^(?:(avg|max):)?((?:[<>])=?[0-9]+%?$)/.exec(s) + const regexResult = /^(?:(avg|max):)([<>]=?[0-9]+%?$)/.exec(s) if (regexResult) { const metricName = regexResult[1] let metric = MetricType.AVERAGE @@ -275,7 +274,7 @@ function getFilteredComparisons(comparisons: ComparisonListElement[]) { }) for (const metric of [MetricType.AVERAGE, MetricType.MAXIMUM]) { for (const search of searchPerMetric[metric]) { - const regexResult = /((?:[<>])=?)([0-9]+)%?/.exec(search)! + const regexResult = /([<>]=?)([0-9]+)%?/.exec(search)! const operator = regexResult[1] const value = parseInt(regexResult[2]) if (evaluateMetricComparison(c.similarities[metric] * 100, operator, value)) { @@ -340,10 +339,25 @@ function getClusterFor(clusterIndex: number) { const displayClusters = props.clusters != undefined -let clusterIconColors = [] as Array +let clusterIconHues = [] as Array +const lightmodeSaturation = 80 +const lightmodeLightness = 50 +const lightmodeAlpha = 0.3 +const darkmodeSaturation = 90 +const darkmodeLightness = 65 +const darkmodeAlpha = 0.6 if (props.clusters != undefined) { - clusterIconColors = generateColors(props.clusters.length, 0.8, 0.5, 1) + clusterIconHues = generateHues(props.clusters.length) } +const clusterIconColors = computed(() => + clusterIconHues.map((h) => { + return `hsla(${h}, ${ + store().uiState.useDarkMode ? darkmodeSaturation : lightmodeSaturation + }%, ${ + store().uiState.useDarkMode ? darkmodeLightness : lightmodeLightness + }%, ${store().uiState.useDarkMode ? darkmodeAlpha : lightmodeAlpha})` + }) +) function isHighlightedRow(item: ComparisonListElement) { return ( @@ -394,16 +408,4 @@ watch( .tableCell { @apply mx-3 flex flex-row items-center justify-center text-center; } - -/* Tooltip arrow. Defined down here bacause of the content attribute */ -.tooltipArrow::after { - content: ' '; - position: absolute; - top: 50%; - left: 100%; - margin-top: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent transparent rgba(0, 0, 0, 0.9); -} diff --git a/report-viewer/src/components/ContainerComponent.vue b/report-viewer/src/components/ContainerComponent.vue index 073f3b50b8..c479b487b5 100644 --- a/report-viewer/src/components/ContainerComponent.vue +++ b/report-viewer/src/components/ContainerComponent.vue @@ -1,9 +1,9 @@ - diff --git a/report-viewer/src/components/VersionRepositoryReference.vue b/report-viewer/src/components/VersionRepositoryReference.vue new file mode 100644 index 0000000000..11f504c072 --- /dev/null +++ b/report-viewer/src/components/VersionRepositoryReference.vue @@ -0,0 +1,40 @@ + + + diff --git a/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue b/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue index d62b14b093..65ee66ed85 100644 --- a/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue +++ b/report-viewer/src/components/distributionDiagram/DistributionDiagram.vue @@ -90,11 +90,11 @@ const options = computed(() => { : 10 ** Math.ceil(Math.log10(maxVal.value + 5)), type: graphOptions.value.xScale, ticks: { - // ensures that in log mode tick labels are not overlappein + // ensures that in log mode tick labels are not overlapping minRotation: graphOptions.value.xScale === 'logarithmic' ? 30 : 0, autoSkipPadding: 10, color: graphColors.ticksAndFont.value, - // ensures that in log mode ticks are placed evenly appart + // ensures that in log mode ticks are placed evenly apart callback: function (value: any) { if (graphOptions.value.xScale === 'logarithmic' && (value + '').match(/1(0)*[^1-9.]/)) { return value diff --git a/report-viewer/src/components/fileDisplaying/CodeLine.vue b/report-viewer/src/components/fileDisplaying/CodeLine.vue index bcced0c92b..ae21ba99cb 100644 --- a/report-viewer/src/components/fileDisplaying/CodeLine.vue +++ b/report-viewer/src/components/fileDisplaying/CodeLine.vue @@ -10,7 +10,7 @@ @@ -57,14 +57,6 @@ function matchSelected(match?: MatchInSingleFile) { const lineRef = ref(null) -function scrollTo() { - if (lineRef.value) { - lineRef.value.scrollIntoView({ block: 'center' }) - } -} - -defineExpose({ scrollTo }) - interface TextPart { line: string match?: MatchInSingleFile diff --git a/report-viewer/src/components/fileDisplaying/CodePanel.vue b/report-viewer/src/components/fileDisplaying/CodePanel.vue index 9ba2ebe7f2..5da443d081 100644 --- a/report-viewer/src/components/fileDisplaying/CodePanel.vue +++ b/report-viewer/src/components/fileDisplaying/CodePanel.vue @@ -4,7 +4,20 @@ @@ -21,7 +21,7 @@ import LoadingCircle from '@/components/LoadingCircle.vue' import { redirectOnError } from '@/router' import { OptionsFactory } from '@/model/factories/OptionsFactory' import type { CliOptions } from '@/model/CliOptions' -import RepositoryReference from '@/components/RepositoryReference.vue' +import VersionRepositoryReference from '@/components/VersionRepositoryReference.vue' const overview: Ref = ref(null) const cliOptions: Ref = ref(undefined) @@ -36,5 +36,5 @@ OverviewFactory.getOverview() OptionsFactory.getCliOptions() .then((o) => (cliOptions.value = o)) - .catch((error) => console.log('Could not load full options.', error)) + .catch((error) => console.error('Could not load full options.', error)) diff --git a/report-viewer/src/viewWrapper/OverviewViewWrapper.vue b/report-viewer/src/viewWrapper/OverviewViewWrapper.vue index 13c0008439..e73d074db5 100644 --- a/report-viewer/src/viewWrapper/OverviewViewWrapper.vue +++ b/report-viewer/src/viewWrapper/OverviewViewWrapper.vue @@ -8,7 +8,7 @@ - + @@ -19,7 +19,7 @@ import OverviewView from '@/views/OverviewView.vue' import type { Overview } from '@/model/Overview' import LoadingCircle from '@/components/LoadingCircle.vue' import { redirectOnError } from '@/router' -import RepositoryReference from '@/components/RepositoryReference.vue' +import VersionRepositoryReference from '@/components/VersionRepositoryReference.vue' const overview: Ref = ref(null) diff --git a/report-viewer/src/views/ComparisonView.vue b/report-viewer/src/views/ComparisonView.vue index c7387d8364..e0a0860db3 100644 --- a/report-viewer/src/views/ComparisonView.vue +++ b/report-viewer/src/views/ComparisonView.vue @@ -73,6 +73,14 @@ :basecode-in-second="secondBaseCodeMatches" @match-selected="showMatch" /> +
@@ -87,6 +95,7 @@ :highlight-language="language" :base-code-matches="firstBaseCodeMatches" @match-selected="showMatchInSecond" + @files-moved="filesMoved()" class="max-h-0 min-h-full flex-1 overflow-hidden print:max-h-none print:overflow-y-visible" /> @@ -122,6 +132,8 @@ import { MetricType } from '@/model/MetricType' import { Comparison } from '@/model/Comparison' import { redirectOnError } from '@/router' import ToolTipComponent from '@/components/ToolTipComponent.vue' +import { FileSortingOptions, fileSortingTooltips } from '@/model/ui/FileSortingOptions' +import OptionsSelectorComponent from '@/components/optionsSelectors/OptionsSelectorComponent.vue' import type { BaseCodeMatch } from '@/model/BaseCodeReport' library.add(faPrint) @@ -154,18 +166,16 @@ const panel1: Ref = ref(null) const panel2: Ref = ref(null) /** - * Shows a match in the first files container when clicked on a line in the second files container. - * @param file (file name) - * @param line (line number) + * Shows a match in the first files container when clicked on a line in the second file container. + * @param match The match to scroll to */ function showMatchInFirst(match: Match) { panel1.value?.scrollTo(match.firstFile, match.startInFirst.line) } /** - * Shows a match in the second files container, when clicked on a line in the second files container. - * @param file (file name) - * @param line (line number) + * Shows a match in the second files container, when clicked on a line in the second file container. + * @param match The match to scroll to */ function showMatchInSecond(match: Match) { panel2.value?.scrollTo(match.secondFile, match.startInSecond.line) @@ -173,7 +183,6 @@ function showMatchInSecond(match: Match) { /** * Shows a match in the first and second files container. - * @param e The click event * @param match The match to show */ function showMatch(match: Match) { @@ -181,12 +190,38 @@ function showMatch(match: Match) { showMatchInSecond(match) } +const sortingOptions = [ + FileSortingOptions.ALPHABETICAL, + FileSortingOptions.MATCH_COVERAGE, + FileSortingOptions.MATCH_COUNT, + FileSortingOptions.MATCH_SIZE +] +const movedAfterSorting = ref(false) +const sortingOptionSelector: Ref = ref(null) + +function changeFileSorting(index: number) { + movedAfterSorting.value = false + if (index < 0) { + return + } + store().uiState.fileSorting = sortingOptions[index] + panel1.value?.sortFiles(store().uiState.fileSorting) + panel2.value?.sortFiles(store().uiState.fileSorting) +} + +function filesMoved() { + movedAfterSorting.value = true + if (sortingOptionSelector.value) { + sortingOptionSelector.value.select(-2) + } +} + function print() { window.print() } // This code is responsible for changing the theme of the highlighted code depending on light/dark mode -// Changing the used style itsself is the desired solution (https://github.com/highlightjs/highlight.js/issues/2115) +// Changing the used style itself is the desired solution (https://github.com/highlightjs/highlight.js/issues/2115) const styleholder: Ref = ref(null) onMounted(() => { diff --git a/report-viewer/src/views/ErrorView.vue b/report-viewer/src/views/ErrorView.vue index 99e284591a..1fce99bb14 100644 --- a/report-viewer/src/views/ErrorView.vue +++ b/report-viewer/src/views/ErrorView.vue @@ -60,7 +60,7 @@ defineProps({ onErrorCaptured((error) => { console.error(error) alert( - 'An error occured that could not be handeled. Please check the console for more information.' + 'An error occurred that could not be handled. Please check the console for more information.' ) return false }) diff --git a/report-viewer/src/views/FileUploadView.vue b/report-viewer/src/views/FileUploadView.vue index 18fb14b05f..02191f8e49 100644 --- a/report-viewer/src/views/FileUploadView.vue +++ b/report-viewer/src/views/FileUploadView.vue @@ -11,12 +11,16 @@
JPlag Logo JPlag Logo
- -
- -
+

{{ getErrorText() }}

For more details check the console.

@@ -58,7 +59,6 @@ import Button from '@/components/ButtonComponent.vue' import VersionInfoComponent from '@/components/VersionInfoComponent.vue' import LoadingCircle from '@/components/LoadingCircle.vue' import { ZipFileHandler } from '@/model/fileHandling/ZipFileHandler' -import { JsonFileHandler } from '@/model/fileHandling/JsonFileHandler' import { BaseFactory } from '@/model/factories/BaseFactory' store().clearStore() @@ -107,21 +107,6 @@ function navigateToOverview() { }) } -/** - * Handles a json file on drop. It read the file and passes the file string to next window. - * @param file The json file to handle - */ -async function handleJsonFile(file: Blob) { - try { - await new JsonFileHandler().handleFile(file) - } catch (e) { - registerError(e as Error, 'upload') - return - } - store().setLoadingType('single') - navigateToOverview() -} - /** * Handles a file on drop. It determines the file type and passes it to the corresponding handler. * @param file File to handle @@ -136,8 +121,6 @@ async function handleFile(file: Blob) { store().setLoadingType('zip') await new ZipFileHandler().handleFile(file) return navigateToOverview() - case 'application/json': - return await handleJsonFile(file) default: throw new Error(`Unknown MIME type '${file.type}'`) } @@ -232,4 +215,8 @@ onErrorCaptured((error) => { registerError(error, 'unknown') return false }) + +if (exampleFiles.value) { + continueWithLocal() +} diff --git a/report-viewer/src/views/InformationView.vue b/report-viewer/src/views/InformationView.vue index 061ee9c6d5..b505e4ec46 100644 --- a/report-viewer/src/views/InformationView.vue +++ b/report-viewer/src/views/InformationView.vue @@ -68,7 +68,7 @@ }}
- {{ + {{ options.clusterOptions.agglomerativeThreshold }} diff --git a/report-viewer/tailwind.config.js b/report-viewer/tailwind.config.js index 94b94d875a..4fc25e8f18 100644 --- a/report-viewer/tailwind.config.js +++ b/report-viewer/tailwind.config.js @@ -11,32 +11,32 @@ export default { light: '#000000', dark: '#ffffff' }, - backgorund: { + background: { light: 'hsl(0, 0%, 97%)', - dark: 'hsl(180, 80%, 3%)' + dark: 'hsl(230, 10%, 8%)' }, container: { light: 'hsl(0, 0%, 98%)', - dark: 'hsl(200, 20%, 13%)', + dark: 'hsl(250, 10%, 15%)', border: { light: 'hsl(0, 0%, 80%)', dark: 'hsl(0, 0%, 25%)' }, secondary: { light: 'hsl(0, 0%, 95%)', - dark: 'hsl(200, 20%, 18%)' + dark: 'hsl(250, 10%, 20%)' } }, interactable: { light: 'hsl(0, 0%, 100%)', - dark: 'hsl(180, 30%, 18%)', + dark: 'hsl(250, 10%, 20%)', border: { light: 'hsl(0, 0%, 75%)', - dark: 'hsl(0, 0%, 30%)' + dark: 'hsl(0, 0%, 25%)' } }, scrollbar: { - backgorund: { + background: { light: colors.slate[100], dark: '#30363D' }, diff --git a/report-viewer/tests/e2e/README.md b/report-viewer/tests/e2e/README.md new file mode 100644 index 0000000000..c5e07d5906 --- /dev/null +++ b/report-viewer/tests/e2e/README.md @@ -0,0 +1,62 @@ +# Complete System e2e Tests + +The e2e tests are executed by the [complete e2e tests workflow](../../../.github/workflows/complete-e2e.yml) and are meant to check the entire process from building and executing JPlag to viewing the report in the report viewer. +The tests get run on 3 different operating systems: Windows, Ubuntu and MacOS. + +## Structure + +First in the `build_jar` job the JPlag jar is built. + +Then in the `run_jplag` job the JPlag jar is executed with the test data. Here using matrix, JPlag is run for each dataset on each operating system. + +Finally in the `e2e_test` job the playwright tests specified here are run. The tests are run on each operating system. + +### Open Comparison Tests + +The `OpenComparisonTest.spec.ts` test, loads each of the specified reports and tries to open its top comparison. This is done to ensure that basic functionality of JPlag is working. +We test that the most used languages are working correctly and that the report viewer does not throw an error opening them. We also test that all ways to give files as single files or folders are exported into their respective reports. + +### Other Tests + +The other tests are testing the functionality of the report viewer. Each of them tests one view and makes sure all the features are working correctly. + +## Running the tests locally + +1) To run the tests locally get the zips of the datasets from the [GitHub](../../../.github/workflows/files/) and execute JPlag on its contents. +2) Build the report viewer using `npm run build` +3) Run the e2e tests using `npm run test:e2e` + +## Adding new tests + +If you want to add new tests we suggest doing the following tests: + +1) Create a dataset you can upload to GitHub + - Copy the zip of the dataset into [the workflow files folder](../../../.github/workflows/files/) + - Execute it on your device, so you can test your new test locally + - If you want to add the dataset to `OpenComparisonTest.spec.ts` make sure there is a clear top comparison and you do not have multiple comparisons with the same percentage as the top comparison +2) Add the test to the matrix in the [complete e2e tests workflow](../../../.github/workflows/complete-e2e.yml) + - zip: The name of the zip file in the files folder + - name: The name of the dataset. This name should be unique + - folder: This is the main folder of the dataset, that gets passed to JPlag as a positional argument + - language: The language JPlag should use. This should be the same name passed to the `-l` parameter + - cliArgs: Additional arguments to pass to JPlag. This could be used to specify basecode or give JPlag more folders over `--new`/`--old` + +3) Add the test to the playwright e2e tests. + - Adding a test to `OpenComparisonTest`: + - Add the dataset name to the `datasets` array + - Specify the name of the zip that should be opened. They follow the pattern `DATASET_NAME-report.zip` + - Specify the names of the submissions of the top comparisons. These are given as regexes + - Adding a completely new Test: + - Create a new file in this folder with the file ending `.spec.ts` + - Add a new test according to the playwright documentation (examples are in the other tests) + - The test should start like this + ```typescript + test('Name of the test', async ({ page }) => { + await page.goto('/') + await uploadFile('YOUR_DATASET_NAME-report.zip', page) + // Your test code + }); + ``` + This will start you on the overview page with the dataset loaded + +4) Run the tests locally to make sure they are working diff --git a/report-viewer/tests/unit/components/VersionInfoComponent.test.ts b/report-viewer/tests/unit/components/VersionInfoComponent.test.ts index 6cbb9966bc..a706ffafe0 100644 --- a/report-viewer/tests/unit/components/VersionInfoComponent.test.ts +++ b/report-viewer/tests/unit/components/VersionInfoComponent.test.ts @@ -19,20 +19,6 @@ describe('VersionInfoComponent', () => { ) }) - it('Render outdated version', async () => { - vi.spyOn(versionTsFile, 'reportViewerVersion', 'get').mockReturnValue(mockVersionJSON(4, 3, 0)) - vi.spyOn(versionTsFile, 'minimalReportVersion', 'get').mockReturnValue(mockVersionJSON(4, 0, 0)) - global.fetch = vi.fn().mockResolvedValueOnce(mockVersionResponse('v4.4.0')) - - const wrapper = mount(VersionInfoComponent) - await flushPromises() - - expect(wrapper.text()).toContain('outdated version') - expect(wrapper.text()).toContain( - 'The minimal version of JPlag that is supported by the viewer is v4.0.0.' - ) - }) - it('Render latest version', async () => { vi.spyOn(versionTsFile, 'reportViewerVersion', 'get').mockReturnValue(mockVersionJSON(4, 3, 0)) vi.spyOn(versionTsFile, 'minimalReportVersion', 'get').mockReturnValue(mockVersionJSON(4, 0, 0)) diff --git a/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts b/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts new file mode 100644 index 0000000000..2866e79a4f --- /dev/null +++ b/report-viewer/tests/unit/components/comparisonTable/ComparisonTable.test.ts @@ -0,0 +1,371 @@ +import ComparisonTable from '@/components/ComparisonsTable.vue' +import { flushPromises, mount } from '@vue/test-utils' +import { describe, it, vi, expect } from 'vitest' +import { createTestingPinia } from '@pinia/testing' +import { store } from '@/stores/store' +import { MetricType } from '@/model/MetricType.ts' +import { router } from '@/router' +import OptionsSelector from '@/components/optionsSelectors/OptionsSelectorComponent.vue' +import OptionComponent from '@/components/optionsSelectors/OptionComponent.vue' + +describe('ComparisonTable', async () => { + it('Test search string filtering', async () => { + const wrapper = mount(ComparisonTable, { + props: { + topComparisons: [ + { + sortingPlace: 0, + id: 1, + firstSubmissionId: 'A', + secondSubmissionId: 'B', + similarities: { + [MetricType.AVERAGE]: 1, + [MetricType.MAXIMUM]: 0.5 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'C', + secondSubmissionId: 'D', + similarities: { + [MetricType.AVERAGE]: 0.5, + [MetricType.MAXIMUM]: 1 + }, + clusterIndex: -1 + } + ], + clusters: [] + }, + global: { + plugins: [getStore(), router] + } + }) + + // check that filtering works with one name + wrapper.find('input').setValue('A') + await flushPromises() + const displayedComparisonsSingleName = wrapper.vm.displayedComparisons + expect(displayedComparisonsSingleName.length).toBe(1) + expect(displayedComparisonsSingleName[0].firstSubmissionId).toBe('A') + expect(displayedComparisonsSingleName[0].secondSubmissionId).toBe('B') + + // check that filtering works with two names + wrapper.find('input').setValue('A D') + await flushPromises() + const displayedComparisonsTwoNames = wrapper.vm.displayedComparisons + expect(displayedComparisonsTwoNames.length).toBe(2) + }) + + it('Test search bar filtering by index', async () => { + const wrapper = mount(ComparisonTable, { + props: { + topComparisons: [ + { + sortingPlace: 0, + id: 1, + firstSubmissionId: 'A', + secondSubmissionId: 'B', + similarities: { + [MetricType.AVERAGE]: 0.3, + [MetricType.MAXIMUM]: 0.5 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'C', + secondSubmissionId: 'D', + similarities: { + [MetricType.AVERAGE]: 0.5, + [MetricType.MAXIMUM]: 1 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'E', + secondSubmissionId: 'F', + similarities: { + [MetricType.AVERAGE]: 0.3, + [MetricType.MAXIMUM]: 0.1 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'H', + secondSubmissionId: 'G', + similarities: { + [MetricType.AVERAGE]: 0.9, + [MetricType.MAXIMUM]: 0.2 + }, + clusterIndex: -1 + } + ], + clusters: [] + }, + global: { + plugins: [getStore(), router] + } + }) + + wrapper.find('input').setValue('2') + await flushPromises() + const displayedComparisonsIndex1 = wrapper.vm.displayedComparisons + expect(displayedComparisonsIndex1.length).toBe(1) + expect(displayedComparisonsIndex1[0].firstSubmissionId).toBe('C') + + wrapper.find('input').setValue('2 3') + await flushPromises() + const displayedComparisonsIndex2 = wrapper.vm.displayedComparisons + expect(displayedComparisonsIndex2.length).toBe(2) + expect(displayedComparisonsIndex2[0].firstSubmissionId).toBe('C') + expect(displayedComparisonsIndex2[1].firstSubmissionId).toBe('A') + + wrapper.find('input').setValue('index:1') + await flushPromises() + const displayedComparisonsIndex3 = wrapper.vm.displayedComparisons + expect(displayedComparisonsIndex3.length).toBe(1) + expect(displayedComparisonsIndex3[0].firstSubmissionId).toBe('H') + + const metricOptions = wrapper.getComponent(OptionsSelector).findAllComponents(OptionComponent) + await metricOptions[1].trigger('click') + wrapper.find('input').setValue('index:2') + await flushPromises() + const displayedComparisonsIndex4 = wrapper.vm.displayedComparisons + expect(displayedComparisonsIndex4.length).toBe(1) + expect(displayedComparisonsIndex4[0].firstSubmissionId).toBe('A') + }) + + it('Test search bar filtering by metric', async () => { + const wrapper = mount(ComparisonTable, { + props: { + topComparisons: [ + { + sortingPlace: 0, + id: 1, + firstSubmissionId: 'A', + secondSubmissionId: 'B', + similarities: { + [MetricType.AVERAGE]: 0.3, + [MetricType.MAXIMUM]: 0.5 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'C', + secondSubmissionId: 'D', + similarities: { + [MetricType.AVERAGE]: 0.4, + [MetricType.MAXIMUM]: 1 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'E', + secondSubmissionId: 'F', + similarities: { + [MetricType.AVERAGE]: 0.3, + [MetricType.MAXIMUM]: 0.1 + }, + clusterIndex: -1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'H', + secondSubmissionId: 'G', + similarities: { + [MetricType.AVERAGE]: 0.9, + [MetricType.MAXIMUM]: 0.2 + }, + clusterIndex: -1 + } + ], + clusters: [] + }, + global: { + plugins: [getStore(), router] + } + }) + + // check that filtering works over all metrics when no metric is specified + wrapper.find('input').setValue('>45') + await flushPromises() + const displayedComparisonsMetricNoPercentage = wrapper.vm.displayedComparisons + expect(displayedComparisonsMetricNoPercentage.length).toBe(3) + + // check that filtering works with and without percentage + wrapper.find('input').setValue('>45%') + await flushPromises() + const displayedComparisonsMetricWithPercentage = wrapper.vm.displayedComparisons + expect(displayedComparisonsMetricWithPercentage.length).toBe(3) + expect(displayedComparisonsMetricWithPercentage).toEqual(displayedComparisonsMetricNoPercentage) + + // check that filtering works on max metric percentage + wrapper.find('input').setValue('max:>45') + await flushPromises() + expect(wrapper.vm.displayedComparisons.length).toBe(2) + + // check that filtering works on average metric percentage + wrapper.find('input').setValue('avg:>45') + await flushPromises() + expect(wrapper.vm.displayedComparisons.length).toBe(1) + + // check that filtering works correctly on greater, greater or equal, less and less or equal + wrapper.find('input').setValue('max:>50') + await flushPromises() + expect(wrapper.vm.displayedComparisons.length).toBe(1) + + wrapper.find('input').setValue('max:>=50') + await flushPromises() + expect(wrapper.vm.displayedComparisons.length).toBe(2) + + wrapper.find('input').setValue('max:<50%') + await flushPromises() + expect(wrapper.vm.displayedComparisons.length).toBe(2) + + wrapper.find('input').setValue('max:<=50') + await flushPromises() + expect(wrapper.vm.displayedComparisons.length).toBe(3) + }) + + it('Test sorting working', async () => { + const wrapper = mount(ComparisonTable, { + props: { + topComparisons: [ + { + sortingPlace: 0, + id: 1, + firstSubmissionId: 'A', + secondSubmissionId: 'B', + similarities: { + [MetricType.AVERAGE]: 0.3, + [MetricType.MAXIMUM]: 0.5 + }, + clusterIndex: 0 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'C', + secondSubmissionId: 'D', + similarities: { + [MetricType.AVERAGE]: 0.5, + [MetricType.MAXIMUM]: 1 + }, + clusterIndex: 1 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'E', + secondSubmissionId: 'F', + similarities: { + [MetricType.AVERAGE]: 0.3, + [MetricType.MAXIMUM]: 0.1 + }, + clusterIndex: 2 + }, + { + sortingPlace: 1, + id: 2, + firstSubmissionId: 'H', + secondSubmissionId: 'G', + similarities: { + [MetricType.AVERAGE]: 0.9, + [MetricType.MAXIMUM]: 0.2 + }, + clusterIndex: -1 + } + ], + clusters: [ + { + averageSimilarity: 0.5, + strength: 0.5, + members: ['A', 'B'] + }, + { + averageSimilarity: 0.6, + strength: 0.5, + members: ['C', 'D'] + }, + { + averageSimilarity: 0.9, + strength: 0.5, + members: ['E', 'F'] + } + ] + }, + global: { + plugins: [getStore(), router] + } + }) + + // Test sorting by average + const displayedComparisonsAverageSorted = wrapper.vm.displayedComparisons + expect(displayedComparisonsAverageSorted[0].firstSubmissionId).toBe('H') + expect(displayedComparisonsAverageSorted[1].firstSubmissionId).toBe('C') + expect(displayedComparisonsAverageSorted[2].firstSubmissionId).toBe('A') + expect(displayedComparisonsAverageSorted[3].firstSubmissionId).toBe('E') + + const metricOptions = wrapper.getComponent(OptionsSelector).findAllComponents(OptionComponent) + await metricOptions[1].trigger('click') + await flushPromises() + + // Test sorting by max + const displayedComparisonsMaxSorted = wrapper.vm.displayedComparisons + expect(displayedComparisonsMaxSorted[0].firstSubmissionId).toBe('C') + expect(displayedComparisonsMaxSorted[1].firstSubmissionId).toBe('A') + expect(displayedComparisonsMaxSorted[2].firstSubmissionId).toBe('H') + expect(displayedComparisonsMaxSorted[3].firstSubmissionId).toBe('E') + + await metricOptions[2].trigger('click') + await flushPromises() + + // Test sorting by cluster + const displayedComparisonsClusterSorted = wrapper.vm.displayedComparisons + expect(displayedComparisonsClusterSorted[0].firstSubmissionId).toBe('E') + expect(displayedComparisonsClusterSorted[1].firstSubmissionId).toBe('C') + expect(displayedComparisonsClusterSorted[2].firstSubmissionId).toBe('A') + }) + + it('Test header prop', async () => { + const headerText = 'Custom Header' + + const wrapper = mount(ComparisonTable, { + props: { + topComparisons: [], + clusters: [], + header: headerText + }, + global: { + plugins: [createTestingPinia({ createSpy: vi.fn }), router] + } + }) + + expect(wrapper.text()).toContain(headerText) + }) +}) + +function getStore() { + const testStore = createTestingPinia({ createSpy: vi.fn }) + store().state.submissionIdsToComparisonFileName.set('A', new Map([['B', 'file1']])) + store().state.submissionIdsToComparisonFileName.set('B', new Map([['A', 'file1']])) + store().state.submissionIdsToComparisonFileName.set('C', new Map([['D', 'file2']])) + store().state.submissionIdsToComparisonFileName.set('D', new Map([['C', 'file2']])) + store().state.submissionIdsToComparisonFileName.set('E', new Map([['F', 'file3']])) + store().state.submissionIdsToComparisonFileName.set('F', new Map([['E', 'file3']])) + store().state.submissionIdsToComparisonFileName.set('G', new Map([['H', 'file4']])) + store().state.submissionIdsToComparisonFileName.set('H', new Map([['G', 'file4']])) + return testStore +} diff --git a/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts b/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts new file mode 100644 index 0000000000..2b11ea6a5d --- /dev/null +++ b/report-viewer/tests/unit/components/comparisonTable/ComparisonTableFilter.test.ts @@ -0,0 +1,189 @@ +import ComparisonTableFilter from '@/components/ComparisonTableFilter.vue' +import { flushPromises, mount } from '@vue/test-utils' +import { describe, it, vi, expect } from 'vitest' +import { createTestingPinia } from '@pinia/testing' +import { store } from '@/stores/store' +import { MetricType } from '@/model/MetricType.ts' +import ButtonComponent from '@/components/ButtonComponent.vue' +import OptionsSelector from '@/components/optionsSelectors/OptionsSelectorComponent.vue' +import OptionComponent from '@/components/optionsSelectors/OptionComponent.vue' + +describe('ComparisonTableFilter', async () => { + it('Test search string updating', async () => { + const wrapper = mount(ComparisonTableFilter, { + props: { + searchString: '', + 'onUpdate:searchString': (e) => wrapper.setProps({ searchString: e }) + }, + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore() + + const searchValue = 'JPlag' + + wrapper.find('input').setValue(searchValue) + await flushPromises() + expect(wrapper.props('searchString')).toBe(searchValue) + }) + + it('Test metric changes', async () => { + const wrapper = mount(ComparisonTableFilter, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore() + + expect(wrapper.text()).toContain('Average') + expect(wrapper.text()).toContain('Maximum') + expect(wrapper.text()).toContain('Cluster') + + const options = wrapper.getComponent(OptionsSelector).findAllComponents(OptionComponent) + + expectHighlighting(0) + + await options[1].trigger('click') + expect(store().uiState.comparisonTableSortingMetric).toBe(MetricType.MAXIMUM) + expect(store().uiState.comparisonTableClusterSorting).toBeFalsy() + expectHighlighting(1) + + await options[2].trigger('click') + expect(store().uiState.comparisonTableSortingMetric).toBe(MetricType.AVERAGE) + expect(store().uiState.comparisonTableClusterSorting).toBeTruthy() + expectHighlighting(2) + + await options[0].trigger('click') + expect(store().uiState.comparisonTableSortingMetric).toBe(MetricType.AVERAGE) + expect(store().uiState.comparisonTableClusterSorting).toBeFalsy() + expectHighlighting(0) + + function expectHighlighting(index: number) { + for (let i = 0; i < options.length; i++) { + if (i == index) { + expect(options[i].classes()).toContain('!bg-accent') + } else { + expect(options[i].classes()).not.toContain('!bg-accent') + } + } + } + }) + + it('Test anonymous button', async () => { + const wrapper = mount(ComparisonTableFilter, { + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore() + + await wrapper.vm.$nextTick() + expect(wrapper.text()).toContain('Hide All') + + await wrapper.getComponent(ButtonComponent).trigger('click') + + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length) + for (const id of store().getSubmissionIds) { + expect(store().state.anonymous).toContain(id) + } + + // Vue does not actually rerender the component, so this is commented out + await wrapper.vm.$nextTick() + expect(wrapper.text()).toContain('Show All') + expect(wrapper.text()).not.toContain('Hide All') + + await wrapper.getComponent(ButtonComponent).trigger('click') + expect(store().state.anonymous.size).toBe(0) + }) + + it('Test deanoymization', async () => { + const wrapper = mount(ComparisonTableFilter, { + props: { + searchString: '', + 'onUpdate:searchString': (e) => wrapper.setProps({ searchString: e }) + }, + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore(true) + + wrapper.find('input').setValue('C') + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length - 1) + expect(store().state.anonymous).not.toContain('C') + }) + + it('Test deanoymization - case insensitive', async () => { + const wrapper = mount(ComparisonTableFilter, { + props: { + searchString: '', + 'onUpdate:searchString': (e) => wrapper.setProps({ searchString: e }) + }, + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore(true) + + wrapper.find('input').setValue('c') + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length - 1) + expect(store().state.anonymous).not.toContain('C') + }) + + it('Test deanoymization - multiple', async () => { + const wrapper = mount(ComparisonTableFilter, { + props: { + searchString: '', + 'onUpdate:searchString': (e) => wrapper.setProps({ searchString: e }) + }, + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore(true) + + wrapper.find('input').setValue('c A') + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length - 2) + expect(store().state.anonymous).not.toContain('C') + expect(store().state.anonymous).not.toContain('A') + }) + + it('Test deanoymization - name with spaces', async () => { + const wrapper = mount(ComparisonTableFilter, { + props: { + searchString: '', + 'onUpdate:searchString': (e) => wrapper.setProps({ searchString: e }) + }, + global: { + plugins: [createTestingPinia({ createSpy: vi.fn })] + } + }) + setUpStore(true) + + wrapper.find('input').setValue('test') + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length) + expect(store().state.anonymous).toContain('test_User') + + wrapper.find('input').setValue('User') + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length) + expect(store().state.anonymous).toContain('test_User') + + wrapper.find('input').setValue('test User') + expect(store().state.anonymous.size).toBe(store().getSubmissionIds.length - 1) + expect(store().state.anonymous).not.toContain('test_User') + }) +}) + +function setUpStore(fillAnonymous = false) { + const submissionsToDisplayNames = new Map() + submissionsToDisplayNames.set('A', 'A') + submissionsToDisplayNames.set('B', 'B') + submissionsToDisplayNames.set('C', 'C') + submissionsToDisplayNames.set('test_User', 'test User') + store().state.fileIdToDisplayName = submissionsToDisplayNames + store().state.anonymous.clear() + if (fillAnonymous) { + store().state.anonymous = new Set(submissionsToDisplayNames.keys()) + } +} diff --git a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts index e91514897d..a918f43edb 100644 --- a/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/ComparisonFactory.test.ts @@ -1,54 +1,59 @@ -import { vi, it, beforeAll, describe, expect } from 'vitest' +import { it, beforeEach, describe, expect } from 'vitest' import validNew from './ValidComparison.json' import { ComparisonFactory } from '@/model/factories/ComparisonFactory' import { store } from '@/stores/store' import { MetricType } from '@/model/MetricType' - -const store = { - state: { - localModeUsed: false, - zipModeUsed: true, - singleModeUsed: false, - files: {} - }, - getComparisonFileName: (id1: string, id2: string) => { - return `${id1}-${id2}.json` - }, - filesOfSubmission: (name: string) => { - return [ - { - name: `${name}/Structure.java`, - value: '' - }, - { - name: `${name}/Submission.java`, - value: '' - } - ] - }, - getSubmissionFile: (id: string, name: string) => { - return { - fileName: name, - submissionId: id, - matchedTokenCount: 0 - } - } -} +import { setActivePinia, createPinia } from 'pinia' describe('Test JSON to Comparison', () => { - beforeAll(() => { - vi.mock('@/stores/store', () => ({ - store: vi.fn(() => { - return store - }) - })) + beforeEach(() => { + setActivePinia(createPinia()) + store().setLoadingType('zip') }) it('Post 5.0', async () => { - store.state.files['root1-root2.json'] = JSON.stringify(validNew) + store().state.files['root1-root2.json'] = JSON.stringify(validNew) + store().state.submissionIdsToComparisonFileName.set( + 'root1', + new Map([['root2', 'root1-root2.json']]) + ) + store().state.submissionIdsToComparisonFileName.set( + 'root2', + new Map([['root1', 'root1-root2.json']]) + ) + store().state.submissions['root1'] = new Map() + store().state.submissions['root1'].set('root1/Structure.java', { + fileName: 'root1/Structure.java', + value: '', + submissionId: 'root1', + matchedTokenCount: 0, + displayName: 'Structure.java' + }) + store().state.submissions['root1'].set('root1/Submission.java', { + fileName: 'root1/Submission.java', + value: '', + submissionId: 'root1', + matchedTokenCount: 0, + displayName: 'Submission.java' + }) + store().state.submissions['root2'] = new Map() + store().state.submissions['root2'].set('root2/Structure.java', { + fileName: 'root2/Structure.java', + value: '', + submissionId: 'root2', + matchedTokenCount: 0, + displayName: 'Structure.java' + }) + store().state.submissions['root2'].set('root2/Submission.java', { + fileName: 'root2/Submission.java', + value: '', + submissionId: 'root2', + matchedTokenCount: 0, + displayName: 'Submission.java' + }) const result = await ComparisonFactory.getComparison( - store.getComparisonFileName('root1', 'root2') + store().getComparisonFileName('root1', 'root2') ) expect(result).toBeDefined() diff --git a/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts b/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts index b6719152c9..5cbc82bee9 100644 --- a/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/OptionsFactory.test.ts @@ -1,30 +1,19 @@ -import { beforeAll, describe, it, vi, expect } from 'vitest' +import { beforeEach, describe, it, expect } from 'vitest' import { OptionsFactory } from '@/model/factories/OptionsFactory' import { ParserLanguage } from '@/model/Language' import { MetricType } from '@/model/MetricType' import validOptions from './ValidOptions.json' - -const store = { - state: { - localModeUsed: false, - zipModeUsed: true, - singleModeUsed: false, - files: { - 'options.json': JSON.stringify(validOptions) - } - } -} +import { setActivePinia, createPinia } from 'pinia' +import { store } from '@/stores/store' describe('Test JSON to Options', async () => { - beforeAll(() => { - vi.mock('@/stores/store', () => ({ - store: vi.fn(() => { - return store - }) - })) + beforeEach(() => { + setActivePinia(createPinia()) + store().setLoadingType('zip') }) it('Test Valid JSON', async () => { + store().state.files['options.json'] = JSON.stringify(validOptions) const result = await OptionsFactory.getCliOptions() expect(result).toEqual({ diff --git a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts index 6852f5fbfd..d5f9fb21dd 100644 --- a/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts +++ b/report-viewer/tests/unit/model/factories/OverviewFactory.test.ts @@ -1,47 +1,30 @@ -import { beforeAll, describe, expect, it, vi } from 'vitest' +import { beforeAll, describe, expect, it, vi, beforeEach } from 'vitest' import { OverviewFactory } from '@/model/factories/OverviewFactory' import { MetricType } from '@/model/MetricType' import { Distribution } from '@/model/Distribution' +import { ParserLanguage } from '@/model/Language' import validNew from './ValidOverview.json' import outdated from './OutdatedOverview.json' - -const store = { - state: { - localModeUsed: false, - zipModeUsed: true, - singleModeUsed: false, - files: {} - }, - saveSubmissionNames: (map) => { - expect(map.has('A')).toBeTruthy() - expect(map.has('B')).toBeTruthy() - expect(map.has('C')).toBeTruthy() - expect(map.has('D')).toBeTruthy() - }, - saveComparisonFileLookup: (map) => { - expect(map.has('A')).toBeTruthy() - expect(map.has('B')).toBeTruthy() - } -} +import { setActivePinia, createPinia } from 'pinia' +import { store } from '@/stores/store' describe('Test JSON to Overview', () => { beforeAll(() => { - vi.mock('@/stores/store', () => ({ - store: vi.fn(() => { - return store - }) - })) - vi.spyOn(global.window, 'alert').mockImplementation(() => {}) }) + beforeEach(() => { + setActivePinia(createPinia()) + store().setLoadingType('zip') + }) + it('Post 5.0', async () => { - store.state.files['overview.json'] = JSON.stringify(validNew) + store().state.files['overview.json'] = JSON.stringify(validNew) expect(await OverviewFactory.getOverview()).toEqual({ _submissionFolderPath: ['files'], _baseCodeFolderPath: '', - _language: 'Javac based AST plugin', + _language: ParserLanguage.JAVA, _fileExtensions: ['.java', '.JAVA'], _matchSensitivity: 9, _dateOfExecution: '12/07/23', @@ -142,7 +125,7 @@ describe('Test JSON to Overview', () => { describe('Outdated JSON to Overview', () => { it('Outdated version', async () => { - store.state.files['overview.json'] = JSON.stringify(outdated) + store().state.files['overview.json'] = JSON.stringify(outdated) expect(() => OverviewFactory.getOverview()).rejects.toThrowError() }) }) diff --git a/report-viewer/tests/unit/model/factories/ValidComparison.json b/report-viewer/tests/unit/model/factories/ValidComparison.json index 57453ca7b4..2698402a20 100644 --- a/report-viewer/tests/unit/model/factories/ValidComparison.json +++ b/report-viewer/tests/unit/model/factories/ValidComparison.json @@ -7,8 +7,8 @@ }, "matches": [ { - "firstFile": "root2\\Structure.java", - "secondFile": "root1\\Structure.java", + "firstFile": "root1\\Structure.java", + "secondFile": "root2\\Structure.java", "startInFirst": { "line": 1, "column": 1, @@ -32,8 +32,8 @@ "tokens": 139 }, { - "firstFile": "root2\\Submission.java", - "secondFile": "root1\\Submission.java", + "firstFile": "root1\\Submission.java", + "secondFile": "root2\\Submission.java", "startInFirst": { "line": 129, "column": 1, @@ -57,8 +57,8 @@ "tokens": 34 }, { - "firstFile": "root2\\Submission.java", - "secondFile": "root1\\Submission.java", + "firstFile": "root1\\Submission.java", + "secondFile": "root2\\Submission.java", "startInFirst": { "line": 165, "column": 1, @@ -82,8 +82,8 @@ "tokens": 33 }, { - "firstFile": "root2\\Submission.java", - "secondFile": "root1\\Submission.java", + "firstFile": "root1\\Submission.java", + "secondFile": "root2\\Submission.java", "startInFirst": { "line": 112, "column": 1, diff --git a/report-viewer/tests/unit/model/factories/ValidOptions.json b/report-viewer/tests/unit/model/factories/ValidOptions.json index 7081e8a80a..1c4fbb595d 100644 --- a/report-viewer/tests/unit/model/factories/ValidOptions.json +++ b/report-viewer/tests/unit/model/factories/ValidOptions.json @@ -1,5 +1,5 @@ { - "language": "Javac based AST plugin", + "language": "java", "min_token_match": 9, "submission_directories": [ ".\\files" diff --git a/report-viewer/tests/unit/model/factories/ValidOverview.json b/report-viewer/tests/unit/model/factories/ValidOverview.json index 44787eea5e..1ddfd238ef 100644 --- a/report-viewer/tests/unit/model/factories/ValidOverview.json +++ b/report-viewer/tests/unit/model/factories/ValidOverview.json @@ -2,7 +2,7 @@ "jplag_version": { "major": 6, "minor": 0, "patch": 0 }, "submission_folder_path": ["files"], "base_code_folder_path": "", - "language": "Javac based AST plugin", + "language": "java", "file_extensions": [".java", ".JAVA"], "submission_id_to_display_name": { "A": "A", "B": "B", "C": "C", "D": "D" }, "submission_ids_to_comparison_file_name": {