diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java b/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java index 4f511ea2e92dec0b9e3e561fc29aeb65cac641f2..d2e9c8b368f30bcca4faf66f446d7c81ca54a30d 100644 --- a/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/Core.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; import ch.qos.logback.core.spi.AppenderAttachable; import com.fasterxml.jackson.databind.ObjectMapper; +import nl.tudelft.ewi.auta.common.model.metric.MetricFixturesProvider; import nl.tudelft.ewi.auta.core.logging.RingbufferAppender; import nl.tudelft.ewi.auta.core.plugin.PluginLoader; import nl.tudelft.ewi.auta.core.plugin.PluginRepository; @@ -352,4 +353,16 @@ public class Core { final var root = (AppenderAttachable) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); return (RingbufferAppender) root.getAppender("ringbuffer"); } + + /** + * A factory returning metric test fixture providers. + * + * @param gson the GSON instance to deserialize fixtures with + * + * @return the fixture provider + */ + @Bean + public MetricFixturesProvider metricFixturesProviderFactory(final Gson gson) { + return new MetricFixturesProvider(gson); + } } diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java index e8852e88723d2a22ffb7a60461f96e2e5c6ddf59..0e10b7fd22a80f1eecbf574195e74fbfe35ad4fb 100644 --- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/AssignmentController.java @@ -4,6 +4,7 @@ import nl.tudelft.ewi.auta.common.model.metric.MetricName; import nl.tudelft.ewi.auta.common.model.metric.MetricSettings; import nl.tudelft.ewi.auta.core.database.AssignmentRepository; import nl.tudelft.ewi.auta.core.model.Assignment; +import nl.tudelft.ewi.auta.core.model.PassingScriptTestCasesParser; import nl.tudelft.ewi.auta.core.response.Response; import nl.tudelft.ewi.auta.core.response.exception.AssignmentAlreadyExistsException; import nl.tudelft.ewi.auta.core.response.exception.InvalidAssignmentNameException; @@ -43,6 +44,10 @@ public class AssignmentController extends ControllerBase { */ private final AssignmentRepository assignmentStore; + /** + * The parser for parsing passing script tests. + */ + private final PassingScriptTestCasesParser passingScriptTestCasesParser; /** * The service that manages course-level security for this controller. @@ -53,11 +58,16 @@ public class AssignmentController extends ControllerBase { * Creates a new assignment controller. * * @param assignmentStore the assignment repository + * @param passingScriptTestCasesParser the parser for parsing passing script tests */ - public AssignmentController(final AssignmentRepository assignmentStore, - final CourseSecuredService securedService) { + public AssignmentController( + final AssignmentRepository assignmentStore, + final PassingScriptTestCasesParser passingScriptTestCasesParser, + final CourseSecuredService securedService + ) { this.assignmentStore = assignmentStore; this.securedService = securedService; + this.passingScriptTestCasesParser = passingScriptTestCasesParser; } /** @@ -196,6 +206,7 @@ public class AssignmentController extends ControllerBase { return ResponseEntity.ok(res); } + @SuppressWarnings("unchecked") private void populateFromRequest(final Map<String, Object> req, final Assignment assignment) { // First extract the mandatory arguments @@ -223,23 +234,19 @@ public class AssignmentController extends ControllerBase { assignment.setName(name); assignment.setAllowedLanguages(Collections.singleton(lang)); assignment.setMetricSettings(stat.stream().map(o -> { - if (MetricName.valueOf((String) o.get("name")) == MetricName.DOCKER_LOGS) { - return new MetricSettings( - MetricName.valueOf((String) o.get("name")), - (String) o.get("script"), - (String) o.get("formatter"), - 0, - 0, - (String) o.get("dockerfile") - ); - } - return new MetricSettings( - MetricName.valueOf((String) o.get("name")), - (String) o.get("script"), - (String) o.get("formatter"), - 0, - 0 - ); + final var passingScriptTests = this.passingScriptTestCasesParser.parseTests( + (List<Map<String, Object>>) o.get("scriptTests") + ); + final var settings = new MetricSettings( + MetricName.valueOf((String) o.get("name")), + (String) o.get("script"), + (String) o.get("formatter"), + 0, + 0, + (String) o.get("dockerfile") + ); + settings.setPassingScriptTestCases(passingScriptTests); + return settings; }).collect(Collectors.toUnmodifiableSet())); } } diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java index d71ff1e8be4baab062640e263607ee7c7d422352..4ee16bfd8149c9bc3ca4ad9f384e3c2282402aa6 100644 --- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ControllerBase.java @@ -43,6 +43,10 @@ public class ControllerBase { } } + public Object getParam(final Map<String, Object> req, final String key) { + return this.getParam(req, key, Object.class); + } + /** * Extracts a string field from the request body. * diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/MetricsController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/MetricsController.java new file mode 100644 index 0000000000000000000000000000000000000000..0ac98698ca3a5ddcf780ba7a6172662f862689f6 --- /dev/null +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/MetricsController.java @@ -0,0 +1,112 @@ +package nl.tudelft.ewi.auta.core.controller; + +import com.google.gson.Gson; +import nl.tudelft.ewi.auta.common.model.metric.MetricFixturesProvider; +import nl.tudelft.ewi.auta.core.communication.MessageReceiver; +import nl.tudelft.ewi.auta.core.response.Response; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.Map; + +/** + * A controller serving information about the metrics supported by the current installation. + */ +@Controller +public class MetricsController extends ControllerBase { + /** + * The message receiver. + */ + private final MessageReceiver messageReceiver; + + /** + * The GSON instance used to serialize the metrics. + */ + private final Gson gson; + + /** + * The provider for test fixtures. + */ + private final MetricFixturesProvider fixturesProvider; + + /** + * Creates a new metrics controller. + * + * @param messageReceiver the message receiver providing metrics known by the workers + * @param gson the GSON instance used to serialize the metrics + * @param fixturesProvider the provider for test fixtures + */ + public MetricsController( + final MessageReceiver messageReceiver, + final Gson gson, + final MetricFixturesProvider fixturesProvider + ) { + this.messageReceiver = messageReceiver; + this.gson = gson; + this.fixturesProvider = fixturesProvider; + } + + /** + * Gets a map of all languages and their metrics. + * + * @return the response + */ + @GetMapping("/api/v1/worker/metrics") + public ResponseEntity<Response> getMetricsAction() { + final var res = new Response(); + res.put("metrics", this.messageReceiver.getMetrics()); + return ResponseEntity.ok(res); + } + + /** + * Returns the metrics as a {@code metrics} property of the global {@code MetricEditor} object. + * + * @return the metrics as an object property + * + * @deprecated originally a hack for the old UI to allow this resource to load in a blocking + * fashion, no longer necessary + */ + @GetMapping("/api/v1/worker/metrics.js") + @Deprecated + public ResponseEntity<String> getMetricsJsAction() { + final var metrics = this.messageReceiver.getMetrics(); + return ResponseEntity.ok() + .contentType(new MediaType("application", "javascript")) + .body("MetricEditor.metrics = " + this.gson.toJson(metrics)); + } + + /** + * Returns the test fixtures for metrics. + * + * The response is a JSON object resembling the following: + * {@code + * { + * "fixtures": { + * "CYCLOMATIC_COMPLEXITY": [ + * 1, + * 7, + * 14, + * 615 + * ], + * "DOCKER_LOGS": [ + * "BUILD SUCCESSFUL", + * "BUILD FAILED" + * ] + * } + * } + * } + * + * It is important to note that the fixtures can be of any type, as long as the type is valid + * for the metric it belongs to. + * + * @return the test fixtures + */ + @GetMapping("/api/v1/worker/metrics/test-fixtures") + public ResponseEntity<Response> getMetricTestFixturesAction() { + return ResponseEntity.ok(new Response(Map.of( + "fixtures", this.fixturesProvider.getFixtures() + ))); + } +} diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationController.java new file mode 100644 index 0000000000000000000000000000000000000000..2925eff18e5f4683ac9ef98c3db10c463f5726aa --- /dev/null +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationController.java @@ -0,0 +1,267 @@ +package nl.tudelft.ewi.auta.core.controller; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase; +import nl.tudelft.ewi.auta.core.model.PassingScriptTestCasesParser; +import nl.tudelft.ewi.auta.srf.iface.Script; +import nl.tudelft.ewi.auta.srf.iface.ScriptExecutionContextFactory; +import nl.tudelft.ewi.auta.srf.model.NoteCollection; +import org.intellij.lang.annotations.Language; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A controller for criteria script validation actions. + */ +@Controller +public class ScriptValidationController extends ControllerBase { + /** + * The factory building script execution contexts. + */ + private final ScriptExecutionContextFactory scriptExecutionContextFactory; + + /** + * The parser for parsing (criteria) script tests. + */ + private final PassingScriptTestCasesParser passingScriptTestCasesParser; + + /** + * Creates a new script validation controller. + * + * @param scriptExecutionContextFactory the factory building script execution contexts + * @param passingScriptTestCasesParser the parser for parsing script tests + */ + public ScriptValidationController( + final ScriptExecutionContextFactory scriptExecutionContextFactory, + final PassingScriptTestCasesParser passingScriptTestCasesParser + ) { + this.scriptExecutionContextFactory = scriptExecutionContextFactory; + this.passingScriptTestCasesParser = passingScriptTestCasesParser; + } + + /** + * Validates a passing script. + * + * @param req the request + * + * @return a test summary + * + * @throws Exception if validation fails for reasons unrelated to the script + */ + @PostMapping("/api/v1/script/test/passing-script") + @SuppressWarnings("unchecked") + @SuppressFBWarnings( + value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", + justification = "SpotBugs is not aware of try-with-resources and its null checks" + ) + public ResponseEntity<Object> validateScript( + final @RequestBody Map<String, Object> req + ) throws Exception { + try (var sec = this.scriptExecutionContextFactory.create()) { + @Language("ECMAScript 6") + final var script = this.getString(req, "script"); + final var rawTests = this.getParam(req, "tests", List.class); + final List<PassingScriptTestCase> tests = + this.passingScriptTestCasesParser.parseTests(rawTests); + + final Script<Object> compiledScript; + try { + compiledScript = sec.submitScript(script, Object.class); + } catch (final Exception ex) { + return ResponseEntity.ok(new TestSuiteResults(ex.getMessage())); + } + + final var results = tests.stream() + .map(this.buildTestRunner(compiledScript)) + .collect(Collectors.toList()); + + return ResponseEntity.ok(new TestSuiteResults(results)); + } + } + + /** + * Builds a test runner. + * + * @param compiledScript the script under testing + * + * @return the function to call on a test case + */ + private Function<PassingScriptTestCase, TestResults> buildTestRunner( + final Script<Object> compiledScript + ) { + return test -> { + assert test.getId() != null; + assert test.getValue() != null; + + final var notes = new NoteCollection(); + final Optional<Double> grade; + try { + grade = compiledScript.execute(test.getValue(), null, notes); + } catch (final Exception ex) { + return new TestResults(test.getId(), ex.getMessage()); + } + + return new TestResults(test.getId(), notes, grade.orElse(null)); + }; + } + + /** + * The results of an entire test suite. + */ + private static final class TestSuiteResults { + /** + * Whether the script under test compiled. + * + * Iff the test compiled, this is {@code true}. Otherwise, this is a string containing a + * message why compilation failed. + */ + private final Object compiles; + + /** + * The list of results for the individual test suites. + */ + private final List<TestResults> results; + + /** + * Constructs a new test suite result collection for a script that fails to compile. + * + * @param compiles the message describing the compilation failure + */ + private TestSuiteResults(final String compiles) { + this.compiles = compiles; + this.results = Collections.emptyList(); + } + + /** + * Creates a new test suite result collection for a script that compiles. + * + * @param results the list of results of the test suites + */ + private TestSuiteResults(final List<TestResults> results) { + this.compiles = true; + this.results = results; + } + + /** + * Returns the object describing the compilation status of the script. + * + * @return the compilation status + */ + public Object getCompiles() { + return this.compiles; + } + + /** + * Returns the results of the individual unit tests. + * + * @return the results + */ + public List<TestResults> getResults() { + return this.results; + } + } + + /** + * The results of a single unit test. + */ + private static final class TestResults { + /** + * The unit test's identifier. + */ + private final String id; + + /** + * The runtime error, if any. + */ + @Nullable + private final String error; + + /** + * The notes generated by the script. + */ + private final NoteCollection notes; + + /** + * The grade generated by the script, if any. + */ + @Nullable + private final Double grade; + + /** + * Creates a new results object. + * + * @param id the identifier of the test that generated these results + * @param notes the notes generated by the script + * @param grade the grade generated by the script, if any + */ + private TestResults( + final String id, final NoteCollection notes, final @Nullable Double grade + ) { + this.id = id; + this.error = null; + this.notes = notes; + this.grade = grade; + } + + /** + * Creates a new results object for a test that generated a run-time error. + * + * @param id the identifier of the test that errored out + * @param error the error generated by the test + */ + private TestResults(final String id, final String error) { + this.id = id; + this.error = error; + this.notes = new NoteCollection(); + this.grade = null; + } + + /** + * Returns the identifier of the test. + * + * @return the identifier + */ + public String getId() { + return this.id; + } + + /** + * Returns the error generated by the test, if any. + * + * @return the error + */ + @Nullable + public String getError() { + return this.error; + } + + /** + * Returns the notes generated by the test. + * + * @return the notes + */ + public NoteCollection getNotes() { + return this.notes; + } + + /** + * Returns the grade generated by the test, if any. + * + * @return the grade + */ + @Nullable + public Double getGrade() { + return this.grade; + } + } +} diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java index c573cb83c9eb8486c37862a8195a9edd90c1bd53..420117d669c354ea737785910cfd5cae8fc71aaa 100644 --- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/WorkerController.java @@ -1,17 +1,12 @@ package nl.tudelft.ewi.auta.core.controller; import nl.tudelft.ewi.auta.core.workers.WorkerPool; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import com.google.gson.Gson; - -import nl.tudelft.ewi.auta.core.communication.MessageReceiver; import nl.tudelft.ewi.auta.core.response.Response; import java.util.HashMap; @@ -24,19 +19,12 @@ public class WorkerController { */ private final WorkerPool workerPool; - /** - * The message receiver. - */ - private MessageReceiver receiver; - /** * Create a new worker controller. * @param workerPool the pool of workers currently connected to the core - * @param receiver the message receiver. */ - public WorkerController(final WorkerPool workerPool, final MessageReceiver receiver) { + public WorkerController(final WorkerPool workerPool) { this.workerPool = workerPool; - this.receiver = receiver; } /** @@ -100,22 +88,4 @@ public class WorkerController { }).collect(Collectors.toList())); return ResponseEntity.ok(res); } - - /** - * Gets a map of all languages and their metrics. - * @return the response - */ - @RequestMapping(path = "/api/v1/worker/metrics", method = RequestMethod.GET) - public ResponseEntity<Response> getMetricsAction() { - final var res = new Response(); - res.put("metrics", this.receiver.getMetrics()); - return ResponseEntity.ok(res); - } - - @GetMapping("/api/v1/worker/metrics.js") - public ResponseEntity<String> getMetricsJsAction() { - return ResponseEntity.ok() - .contentType(new MediaType("application", "javascript")) - .body("MetricEditor.metrics = " + new Gson().toJson(this.receiver.getMetrics())); - } } diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/model/PassingScriptTestCasesParser.java b/core/src/main/java/nl/tudelft/ewi/auta/core/model/PassingScriptTestCasesParser.java new file mode 100644 index 0000000000000000000000000000000000000000..7636419df3dbd816b3efc465353e8b239a96241c --- /dev/null +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/model/PassingScriptTestCasesParser.java @@ -0,0 +1,42 @@ +package nl.tudelft.ewi.auta.core.model; + +import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase; +import nl.tudelft.ewi.auta.core.controller.ControllerBase; +import org.jetbrains.annotations.Contract; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A parser for parsing script tests. + */ +@Service +public class PassingScriptTestCasesParser extends ControllerBase { + /** + * Parses a list of unit tests. + * + * @param tests the object to parse + * + * @return the parsed tests + */ + public List<PassingScriptTestCase> parseTests(final List<Map<String, Object>> tests) { + return tests.stream().map(this::parseTest).collect(Collectors.toUnmodifiableList()); + } + + /** + * Parses a single unit test. + * + * @param test the object to parse + * + * @return the parsed test + */ + @Contract("_ -> new") + public PassingScriptTestCase parseTest(final Map<String, Object> test) { + final var id = this.getString(test, "id"); + final var value = this.getParam(test, "value"); + + return new PassingScriptTestCase(id, value); + } +} diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/MetricsControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/MetricsControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bf46277ae8cb43ac8d80d299feadf94fcdfe0626 --- /dev/null +++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/MetricsControllerTest.java @@ -0,0 +1,66 @@ +package nl.tudelft.ewi.auta.core.controller; + +import com.google.gson.Gson; +import nl.tudelft.ewi.auta.common.model.metric.MetricFixturesProvider; +import nl.tudelft.ewi.auta.core.communication.MessageReceiver; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class MetricsControllerTest { + private MessageReceiver receiver; + private MetricFixturesProvider fixturesProvider; + private MetricsController controller; + private MockMvc mvc; + + @BeforeEach + public void before() { + this.receiver = mock(MessageReceiver.class); + this.fixturesProvider = new MetricFixturesProvider(new Gson()); + + final var metrics = Map.of( + "java", Set.of("cyclomatic complexity"), + "*", Set.of("method length") + ); + + when(this.receiver.getMetrics()).thenReturn((List) Collections.singletonList(metrics)); + + this.controller = new MetricsController(this.receiver, new Gson(), this.fixturesProvider); + + this.mvc = MockMvcBuilders.standaloneSetup(this.controller) + .setControllerAdvice(new RestExceptionHandler()) + .build(); + } + + @Test + public void testGetMetrics() throws Exception { + this.mvc.perform(request(HttpMethod.GET, "/api/v1/worker/metrics")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.errors", hasSize(0))) + .andExpect(jsonPath("$.metrics", hasSize(1))); + } + + @Test + public void testGetMetricFixtures() throws Exception { + this.mvc.perform(get("/api/v1/worker/metrics/test-fixtures")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.fixtures", not(empty()))); + } +} diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9db563d13f2ffadd6ff8a19cbe395bffe0b6cc80 --- /dev/null +++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/ScriptValidationControllerTest.java @@ -0,0 +1,160 @@ +package nl.tudelft.ewi.auta.core.controller; + +import com.google.gson.Gson; +import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase; +import nl.tudelft.ewi.auta.core.model.PassingScriptTestCasesParser; +import nl.tudelft.ewi.auta.srf.iface.Script; +import nl.tudelft.ewi.auta.srf.iface.ScriptExecutionContext; +import nl.tudelft.ewi.auta.srf.iface.ScriptExecutionContextFactory; +import nl.tudelft.ewi.auta.srf.model.Note; +import nl.tudelft.ewi.auta.srf.model.NoteCollection; +import nl.tudelft.ewi.auta.srf.model.Severity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class ScriptValidationControllerTest { + private ScriptExecutionContextFactory scriptExecutionContextFactory; + private ScriptExecutionContext scriptExecutionContext; + private Script<Object> compiledScript; + private PassingScriptTestCasesParser passingScriptTestCasesParser; + private ScriptValidationController controller; + private MockMvc mvc; + + @BeforeEach + public void before() { + this.scriptExecutionContextFactory = mock(ScriptExecutionContextFactory.class); + this.passingScriptTestCasesParser = mock(PassingScriptTestCasesParser.class); + this.scriptExecutionContext = mock(ScriptExecutionContext.class); + this.compiledScript = mock(Script.class); + + doReturn(this.scriptExecutionContext).when(this.scriptExecutionContextFactory).create(); + + this.controller = new ScriptValidationController( + this.scriptExecutionContextFactory, this.passingScriptTestCasesParser + ); + this.mvc = MockMvcBuilders.standaloneSetup(this.controller) + .setControllerAdvice(new RestExceptionHandler()) + .build(); + } + + @Test + public void testMissingBody() throws Exception { + this.mvc.perform(post("/api/v1/script/test/passing-script")) + .andExpect(status().isBadRequest()); + } + + @Test + public void testMissingScript() throws Exception { + this.mvc.perform(post("/api/v1/script/test/passing-script") + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(Map.of("scriptTests", Collections.emptyList()))) + ).andExpect(status().isBadRequest()); + } + + @Test + public void testMissingTests() throws Exception { + this.mvc.perform(post("/api/v1/script/test/passing-script") + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(Map.of("script", "this is Javascript, not Perl"))) + ).andExpect(status().isBadRequest()); + } + + @Test + public void testEmptyTestSuite() throws Exception { + doReturn(this.compiledScript).when(this.scriptExecutionContext).submitScript(any(), any()); + + this.mvc.perform(post("/api/v1/script/test/passing-script") + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(Map.of( + "script", "this is valid Javascript I swear", + "tests", Collections.emptyList() + ))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.compiles", equalTo(true))) + .andExpect(jsonPath("$.results", hasSize(0))); + + verify(this.compiledScript, never()).execute(any(), any(), any()); + } + + @Test + public void testBadScript() throws Exception { + final String complaint = "that is Perl, I ain't gonna touch that"; + + doThrow(new RuntimeException(complaint)) + .when(this.scriptExecutionContext).submitScript(any(), any()); + + this.mvc.perform(post("/api/v1/script/test/passing-script") + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(Map.of( + "script", "\uD83C\uDDF9\uD83C\uDDF7", + "tests", Collections.emptyList() + ))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.compiles", equalTo(complaint))) + .andExpect(jsonPath("$.results", hasSize(0))); + } + + @Test + public void testScriptWithTests() throws Exception { + final var tests = List.of( + new PassingScriptTestCase("first", 12), new PassingScriptTestCase("second", -3) + ); + doReturn(tests).when(this.passingScriptTestCasesParser).parseTests(any()); + doReturn(this.compiledScript).when(this.scriptExecutionContext).submitScript(any(), any()); + + doAnswer(invocation -> { + final Object value = invocation.getArgument(0); + final NoteCollection notes = invocation.getArgument(2); + + if (Objects.equals(value, -3)) { + notes.addNote(new Note(Severity.FAILURE, "it's bad")); + } else { + throw new RuntimeException("invalid"); + } + + return Optional.empty(); + }).when(this.compiledScript).execute(any(), any(), any()); + + this.mvc.perform(post("/api/v1/script/test/passing-script") + .contentType(MediaType.APPLICATION_JSON) + .content(new Gson().toJson(Map.of( + "script", "100% pure Javascript", + "tests", tests + ))) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.compiles", equalTo(true))) + .andExpect(jsonPath("$.results[0].id", equalTo("first"))) + .andExpect(jsonPath("$.results[0].error", equalTo("invalid"))) + .andExpect(jsonPath("$.results[1].id", equalTo("second"))) + .andExpect(jsonPath("$.results[1].error", nullValue())) + .andExpect(jsonPath("$.results[1].notes.notes[0].severity", equalTo("FAILURE"))) + .andExpect(jsonPath("$.results[1].notes.notes[0].message", equalTo("it's bad"))) + .andExpect(jsonPath("$.results[1].grade", nullValue())); + } +} diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java index 9013cf575686ac8edb4c5f2c74c9b36ecad299f9..2fc7234cd2a2df198d17b71cd4e0c5be14a06fed 100644 --- a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java +++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/WorkerControllerTest.java @@ -7,10 +7,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; import nl.tudelft.ewi.auta.common.jump.Jump; import nl.tudelft.ewi.auta.core.model.Assignment; @@ -28,15 +24,10 @@ import org.springframework.http.HttpMethod; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import nl.tudelft.ewi.auta.core.communication.MessageReceiver; - public class WorkerControllerTest { private MockMvc mvc; - @Mock - private MessageReceiver receiver; - @Mock private WorkerPool workerPool; @@ -53,27 +44,12 @@ public class WorkerControllerTest { public void before() { MockitoAnnotations.initMocks(this); - Map<String, Set<String>> metrics = new HashMap<>(); - metrics.put("java", Set.of("cyclomatic complexity")); - metrics.put("*", Set.of("method length")); - - Mockito.when(this.receiver.getMetrics()) - .thenReturn((List) Collections.singletonList(metrics)); - Mockito.when(this.assignment.getId()).thenReturn("123"); Mockito.when(this.submission.getId()).thenReturn("456"); this.mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); } - @Test - public void testAddSubmission() throws Exception { - this.mvc.perform(request(HttpMethod.GET, "/api/v1/worker/metrics")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.errors", hasSize(0))) - .andExpect(jsonPath("$.metrics", hasSize(1))); - } - @Test public void testGetWorkers() throws Exception { final var testWorker = new WorkerConnection(null, "test #1", new Jump()); diff --git a/doc/api/proposal/index.html b/doc/api/proposal/index.html index e68ef650fabfc34e1d471b967d50b2784a6069dc..9436dce9c5d7d98f60be9d53267c29508d6e2400 100644 --- a/doc/api/proposal/index.html +++ b/doc/api/proposal/index.html @@ -206,35 +206,6 @@ Content-Type: application/json <p>Returns a list of normal users.</p> </section> - <section id="f-api-metrisjs"> - <h1><span class="method">GET</span> /api/v1/worker/metrics.js</h1> - <div> - <table> - <tr> - <th>Property</th> - <th>Type</th> - <th>Required</th> - <th>Description</th> - </tr> - <tr> - <td>token</td> - <td>string</td> - <td>yes</td> - <td>the token to invalidate</td> - </tr> - </table> - </div> - <h2>Example response</h2> - <response> -{ - "java": ["LINES_OF_CODE", "CYCLOMATIC_COMPLEXITY"], - "python": ["LINES_OF_CODE"], - "assembly": ["RECURSION"] -} - </response> - <p>Gets a map of all languages and their metrics.</p> - </section> - <section id="f-api-user-logout"> <h1><span class="method">DELETE</span> /api/v1/user/logout</h1> @@ -2150,6 +2121,196 @@ UEsDBBQAAAAAAGdjOEoAAAAAAAAAAAAAAAAFAAAAam... </div> </section> + <section id="f-api-worker-metrisjs"> + <h1><span class="method">GET</span> <del>/api/v1/worker/metrics.js</del></h1> + <div> + <table> + <tr> + <th>Property</th> + <th>Type</th> + <th>Required</th> + <th>Description</th> + </tr> + <tr> + <td>token</td> + <td>string</td> + <td>yes</td> + <td>the user's authentication token</td> + </tr> + </table> + </div> + <h2>Example response</h2> + <response class="javascript"> +MetricEditor.metrics = { + "metrics": { + "python": [ + "cyclomatic complexity", + "method length" + ], + "cpp": [ + "cyclomatic complexity", + "method length", + "lines of code" + ], + "*": [ + "comment count", + "line length" + ] + }, + "errors": [] +} + </response> + <p>Gets a map of all languages and their metrics.</p> + <p> + <strong>Deprecated</strong> - originally a hack for the old UI to allow this resource + to load in a blocking fashion, no longer necessary + </p> + </section> + + <section id="f-api-worker-metrics-test-fixtures"> + <h1><span class="method">GET</span> /api/v1/worker/metrics/test-fixtures</h1> + <div> + <table> + <tr> + <th>Property</th> + <th>Type</th> + <th>Required</th> + <th>Description</th> + </tr> + <tr> + <td>token</td> + <td>string</td> + <td>yes</td> + <td>the user's authentication token</td> + </tr> + </table> + </div> + <h2>Example response</h2> + <response> +{ + "fixtures": { + "CYCLOMATIC_COMPLEXITY": [ + 1, + 7, + 14, + 615 + ], + "DOCKER_LOGS": [ + "BUILD SUCCESSFUL", + "BUILD FAILED" + ] + } +} + </response> + <p>Returns a map containing test fixtures for criteria scripts.</p> + <p> + It is important to note that the fixtures can be of any type, as long as the type is valid + for the metric it belongs to. + </p> + </section> + + <section id="f-api-script-test-passing-script"> + <h1><span class="method">POST</span> /api/v1/script/test/passing-script</h1> + <div> + <h2>Request</h2> + <table> + <tr> + <th>Property</th> + <th>Type</th> + <th>Required</th> + <th>Description</th> + </tr> + <tr> + <td>token</td> + <td>string</td> + <td>yes</td> + <td>the user's authentication token</td> + </tr> + <tr> + <td>script</td> + <td>string</td> + <td>yes</td> + <td>the script to test</td> + </tr> + <tr> + <td>tests</td> + <td>list<spec></td> + <td>yes</td> + <td>the tests to run</td> + </tr> + </table> + <p> + A unit test specification ("spec") is an object containing an identifier <tt>id</tt> + for the test and a value <tt>value</tt> to pass to the script under test. + </p> + </div> + + <div> + <h2>Example request</h2> + <request> +{ + "script": "(() => count => {\n const minWarnThreshold = 4;\n const minFailThreshold = 2;\n const maxWarnThreshold = 20;\n const maxFailThreshold = 30;\n if (count > 0) {\n if (count < minFailThreshold) {\n fail(`Has too few test methods:' ${count} < ${minFailThreshold}`);\n } else if (count < minWarnThreshold) {\n warn(`Has few test methods:' ${count} < ${minWarnThreshold}`);\n } else if (count > maxFailThreshold) {\n fail(`Has too many test methods:' ${count} > ${maxFailThreshold}`);\n } else if (count > maxWarnThreshold) {\n warn(`Has many methods:' ${count} > ${maxWarnThreshold}`);\n }\n }\n})();\n", + "tests": [ + { + "id": "test:t16oyi-k3m42o-ie108e-318ntg", + "value": "3" + }, + { + "id": "test:va47q4-7ti1jg-5ubb30-1xllc6", + "value": "24" + } + ] +} + </request> + </div> + + <div> + <h2>Example response</h2> + <response> +{ + "compiles": true, + "results": [ + { + "id": "test:t16oyi-k3m42o-ie108e-318ntg", + "error": null, + "notes": { + "notes": [ + { + "severity": "WARNING", + "message": "Has few test methods:' 3 < 4" + } + ] + }, + "grade": null + }, + { + "id": "test:va47q4-7ti1jg-5ubb30-1xllc6", + "error": null, + "notes": { + "notes": [ + { + "severity": "WARNING", + "message": "Has many methods:' 24 > 20" + } + ] + }, + "grade": null + } + ] +} + </response> + </div> + + <div> + <p>Validates a passing script.</p> + <p> + The script is checked whether it compiles (mostly for syntax errors) and is then run + against the provided values. The results are returned, along with any errors generated + when running the test, as if the script were run on an entity. + </p> + </div> + </section> + <section id="f-api-get-workers"> <h1><span class="method">GET</span> /api/v1/workers</h1> diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCase.java b/src/main/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCase.java new file mode 100644 index 0000000000000000000000000000000000000000..7485244cf40d8f3e2326c1e0c489fff15f686753 --- /dev/null +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCase.java @@ -0,0 +1,94 @@ +package nl.tudelft.ewi.auta.common.model; + +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A test for passing (criteria) scripts. + */ +public class PassingScriptTestCase { + /** + * The identifier of the test. + */ + @Nullable + private String id; + + /** + * The value to pass to the script. + */ + @Nullable + private Object value; + + /** + * Creates a new passing script test. + */ + public PassingScriptTestCase() { + this.id = null; + this.value = null; + } + + /** + * Creates a new passing script test. + * + * @param id the identifier of the test + * @param value the value to pass to the script + */ + public PassingScriptTestCase(final String id, final Object value) { + this.id = id; + this.value = value; + } + + /** + * Returns the test's identifier. + * + * @return the identifier + */ + @Nullable + public String getId() { + return this.id; + } + + /** + * Sets the test's identifier. + * + * @param id the identifier + */ + public void setId(final @Nullable String id) { + this.id = id; + } + + /** + * Returns the value to pass to the script. + * + * @return the value + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Sets the value to pass to the script. + * + * @param value the value + */ + public void setValue(final @Nullable Object value) { + this.value = value; + } + + @Override + public final boolean equals(final Object obj) { + if (!(obj instanceof PassingScriptTestCase)) { + return false; + } + + final var other = (PassingScriptTestCase) obj; + + return Objects.equals(this.id, other.id) && Objects.equals(this.value, other.value); + } + + @Override + public final int hashCode() { + return Objects.hash(this.id, this.value); + } +} diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java index fcaa5fdcdb7f7efdc414739f4f331cbadde2f4f2..1606781f83cac607f29938bf95f8a3fe70a50f44 100644 --- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/DoubleMetric.java @@ -1,6 +1,6 @@ package nl.tudelft.ewi.auta.common.model.metric; -public class DoubleMetric extends Metric<Double> { +public class DoubleMetric extends NumberMetric<Double> { /** * Create a metric with a value and a name. * diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java index 1dd573753ccc1a2c16c656e2a6eec7b812156395..3a1707fa7866863aa418ba366cd433056f871e7a 100644 --- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/IntegerMetric.java @@ -1,6 +1,6 @@ package nl.tudelft.ewi.auta.common.model.metric; -public class IntegerMetric extends Metric<Integer> { +public class IntegerMetric extends NumberMetric<Integer> { /** * Create a metric with a value and a name. * diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java index e01c04c8c706fdd56a2f406b9797d5ee67b64025..c5f088eeadd670cbe4d6db654cbf9483a2336a93 100644 --- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/Metric.java @@ -1,11 +1,14 @@ package nl.tudelft.ewi.auta.common.model.metric; import org.jetbrains.annotations.Contract; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Objects; public abstract class Metric<T> { + private static final Logger logger = LoggerFactory.getLogger(Metric.class); /** * The value of the metric. @@ -32,6 +35,18 @@ public abstract class Metric<T> { * @param name the name of the metric */ public Metric(final T value, final MetricName name) { + if (!name.isMetricCompatible(this)) { + logger.warn( + "A metric instance with name {} was given an instance of {}, " + + "while it expects instances of {}. This is allowed in the worker, " + + "but reporting scripts may fail due to the mismatch.", + name, this.getClass().getCanonicalName(), name.getType().getCanonicalName() + ); + if (logger.isDebugEnabled()) { + logger.debug("This warning was issued due to:", new Throwable()); + } + } + this.value = value; this.name = name; } diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProvider.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..fcc2933a44e1532cbd6052a223615345bc80580f --- /dev/null +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProvider.java @@ -0,0 +1,62 @@ +package nl.tudelft.ewi.auta.common.model.metric; + +import com.google.gson.Gson; +import nl.tudelft.ewi.auta.common.threads.CheckedExceptionTunnel; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * A provider class for testing metrics. + * + * These values are meant to be used when generating criteria script unit tests. The values were + * picked to be both edge-cases and normal values to fully cover the range of decisions a script + * could take. + */ +public class MetricFixturesProvider { + /** + * The fixtures container the values are stored in. + */ + private final MetricFixturesContainer container; + + /** + * Creates a new metric fixtures provider. + * + * @param gson the GSON instance to deserialize the fixtures with + */ + public MetricFixturesProvider(final Gson gson) { + try (var in = new InputStreamReader( + MetricFixturesProvider.class.getResourceAsStream("test-fixtures.json"), + StandardCharsets.UTF_8 + )) { + this.container = gson.fromJson(in, MetricFixturesContainer.class); + } catch (final IOException ex) { + throw new CheckedExceptionTunnel(ex); + } + } + + /** + * Returns the test fixtures. + * + * @return the fixtures + */ + public Map<MetricName, List<Object>> getFixtures() { + return this.container.fixtures; + } + + /** + * The container class the fixtures are stored in. + * + * This class describes the file format for the serialized fixtures. + */ + private static final class MetricFixturesContainer { + /** + * The map of fixtures. + */ + private final Map<MetricName, List<Object>> fixtures = Collections.emptyMap(); + } +} diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java index 25af88eb0fb337c82469bcb823fc80f5f80798fb..08df3757d2f7f9038de7a456303b3b6a90bbf248 100644 --- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java @@ -1,5 +1,9 @@ package nl.tudelft.ewi.auta.common.model.metric; +import org.jetbrains.annotations.Contract; + +import javax.annotation.Nullable; + /** * The names of the metrics. */ @@ -8,388 +12,425 @@ public enum MetricName { /** * The number of instructions in an assembly file. */ - ASSEMBLY_INSTRUCTION_COUNT, + ASSEMBLY_INSTRUCTION_COUNT(IntegerMetric.class), /** * The recursion targets for an assembly file. */ - ASSEMBLY_RECURSION_TARGET, + ASSEMBLY_RECURSION_TARGET(StringSetMetric.class), /** * The number of assertions per test method. */ - ASSERTIONS_PER_TEST, + ASSERTIONS_PER_TEST(IntegerMetric.class), /** * The number of commented lines in a file. */ - COMMENTED_LINE_COUNT, + COMMENTED_LINE_COUNT(IntegerMetric.class), /** * The number of constructors in a file or class. */ - CONSTRUCTOR_COUNT, + CONSTRUCTOR_COUNT(IntegerMetric.class), /** * The cyclomatic complexity of a method. */ - CYCLOMATIC_COMPLEXITY, + CYCLOMATIC_COMPLEXITY(NumberMetric.class), /** * The logs of the docker container. */ - DOCKER_LOGS, + DOCKER_LOGS(StringMetric.class), /** * The number of fields in a file or class. */ - FIELD_COUNT, + FIELD_COUNT(IntegerMetric.class), /** * The number of files in a project. */ - FILE_COUNT, + FILE_COUNT(IntegerMetric.class), /** * Whether the method has javadocs. */ - JAVADOC_EXISTS, + JAVADOC_EXISTS(BooleanMetric.class), /** * The set of javadoc violations (tags). */ - JAVADOC_VIOLATIONS, + JAVADOC_VIOLATIONS(StringSetMetric.class), /** * The length of each line in a file. */ - LINE_LENGTH, + LINE_LENGTH(IntegerListMetric.class), /** * The number of lines of code in a code block. */ - LINES_OF_CODE, + LINES_OF_CODE(IntegerMetric.class), /** * The number of methods in a file or class. */ - METHOD_COUNT, + METHOD_COUNT(IntegerMetric.class), /** * The number of effective lines of code in each method. This is excluding commented lines and * blank lines. */ - METHOD_EFFECTIVE_LOC, + METHOD_EFFECTIVE_LOC(IntegerMetric.class), /** * The number of parameters in a method. */ - PARAMETER_COUNT, + PARAMETER_COUNT(IntegerMetric.class), /** * The percentage of commented liens in a file. */ - PERCENTAGE_COMMENTED_LINES, + PERCENTAGE_COMMENTED_LINES(DoubleMetric.class), /** * The number of methods with a Test annotation. */ - TEST_METHOD_COUNT, + TEST_METHOD_COUNT(IntegerMetric.class), /** * The UML SVG file as a string. */ - UML, + UML(StringMetric.class), /** * The maintainability index. */ - MAINTAINABILITY_INDEX, + MAINTAINABILITY_INDEX(DoubleMetric.class), /** * The number of unique words in a class or method. */ - UNIQUE_WORDS_COUNT, + UNIQUE_WORDS_COUNT(IntegerMetric.class), /** * The number of lambdas in a class or method. */ - LAMBDAS_COUNT, + LAMBDAS_COUNT(IntegerMetric.class), /** * The number of subclasses in a class or method. */ - SUBCLASSES_COUNT, + SUBCLASSES_COUNT(IntegerMetric.class), /** * The number of anonymous classes in a class or method. */ - ANONYMOUS_CLASSES_COUNT, + ANONYMOUS_CLASSES_COUNT(IntegerMetric.class), /** * The maximum number of nested blocks in a class or method. */ - MAX_NESTED_BLOCKS, + MAX_NESTED_BLOCKS(IntegerMetric.class), /** * The number of math operations in a class or method. */ - MATH_OPERATIONS_COUNT, + MATH_OPERATIONS_COUNT(IntegerMetric.class), /** * The number of assignments in a class or method. */ - ASSIGNMENTS_COUNT, + ASSIGNMENTS_COUNT(IntegerMetric.class), /** * The amount of numbers in a class or method. */ - NUMBER_COUNT, + NUMBER_COUNT(IntegerMetric.class), /** * The amount of string literals in a class or method. */ - STRING_LITERAL_COUNT, + STRING_LITERAL_COUNT(IntegerMetric.class), /** * The number of parenthesized expressions in a class or method. */ - PARENTHESIZED_EXPRESSION_COUNT, + PARENTHESIZED_EXPRESSION_COUNT(IntegerMetric.class), /** * The number of try catch blocks in a class or method. */ - TRY_CATCH_COUNT, + TRY_CATCH_COUNT(IntegerMetric.class), /** * The number of comparisons in a class or method. */ - COMPARISON_COUNT, + COMPARISON_COUNT(IntegerMetric.class), /** * The number of loops in a class or method. */ - LOOP_COUNT, + LOOP_COUNT(IntegerMetric.class), /** * The number of variables used in a class or method. */ - VARIABLES_COUNT, + VARIABLES_COUNT(IntegerMetric.class), /** * The number of return calls in a class or method. */ - RETURN_COUNT, + RETURN_COUNT(IntegerMetric.class), /** *The number of branch instructions in a class or method. */ - WEIGHT_METHOD_CLASS, + WEIGHT_METHOD_CLASS(IntegerMetric.class), /** * The number of unique invocations in a class or method. */ - RESPONSE_FOR_A_CLASS, + RESPONSE_FOR_A_CLASS(IntegerMetric.class), /** * Counts the number of dependencies a class has. */ - COUPLING_BETWEEN_OBJECTS, + COUPLING_BETWEEN_OBJECTS(IntegerMetric.class), /** * Counts how many times each variable is used. */ - VARIABLES_USAGE, + VARIABLES_USAGE(StringToIntegerMetric.class), /** * Counts how many times each field is used. */ - FIELD_USAGE, + FIELD_USAGE(StringToIntegerMetric.class), /** * Counts the number of static methods. */ - STATIC_METHOD_COUNT, + STATIC_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of public methods. */ - PUBLIC_METHOD_COUNT, + PUBLIC_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of private methods. */ - PRIVATE_METHOD_COUNT, + PRIVATE_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of protected methods. */ - PROTECTED_METHOD_COUNT, + PROTECTED_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of synchronized methods. */ - SYNCHRONIZED_METHOD_COUNT, + SYNCHRONIZED_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of final methods. */ - FINAL_METHOD_COUNT, + FINAL_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of default methods. */ - DEFAULT_METHOD_COUNT, + DEFAULT_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of abstract methods. */ - ABSTRACT_METHOD_COUNT, + ABSTRACT_METHOD_COUNT(IntegerMetric.class), /** * Counts the number of public fields. */ - PUBLIC_FIELD_COUNT, + PUBLIC_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of private fields. */ - PRIVATE_FIELD_COUNT, + PRIVATE_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of protected fields. */ - PROTECTED_FIELD_COUNT, + PROTECTED_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of synchronized fields. */ - SYNCHRONIZED_FIELD_COUNT, + SYNCHRONIZED_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of final fields. */ - FINAL_FIELD_COUNT, + FINAL_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of default fields. */ - DEFAULT_FIELD_COUNT, + DEFAULT_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of abstract fields. */ - ABSTRACT_FIELD_COUNT, + ABSTRACT_FIELD_COUNT(IntegerMetric.class), /** * Counts the number of static fields. */ - STATIC_FIELD_COUNT, + STATIC_FIELD_COUNT(IntegerMetric.class), /** * Counts how many time a static invocation is performed in a class. */ - NUMBER_OF_STATIC_INVOCATIONS, + NUMBER_OF_STATIC_INVOCATIONS(IntegerMetric.class), /** * Calculates the lack of cohesion between methods. */ - LACK_OF_COHESION_OF_METHODS, + LACK_OF_COHESION_OF_METHODS(IntegerMetric.class), /** * Calculates the depth of the inheritance tree. */ - DEPTH_INHERITANCE_TREE, + DEPTH_INHERITANCE_TREE(IntegerMetric.class), /** * A list of test smells. */ - TEST_SMELLS, + TEST_SMELLS(StringSetMetric.class), /** * A PyLint result. */ - PYLINT, + PYLINT(PyLintResultMetric.class), /** * A list of duplicate code blocks. */ - DUPLICATE_CODE_BLOCKS, + DUPLICATE_CODE_BLOCKS(DuplicateCodeBlockListMetric.class), /** * Suppressed files or entities. */ - SUPPRESSIONS, + SUPPRESSIONS(SuppressedEntityCollectionMetric.class), /** * The number of effective LOC in a file. That is, excluding both comment lines and blank lines. */ - FILE_EFFECTIVE_LOC, + FILE_EFFECTIVE_LOC(IntegerMetric.class), /** * The total number of assertions divided by the effective lines of source code. */ - RELATIVE_NUMBER_OF_ASSERTIONS, + RELATIVE_NUMBER_OF_ASSERTIONS(DoubleMetric.class), /** * The total number of testcases divided by the effective lines of source code. */ - RELATIVE_NUMBER_OF_TESTCASES, + RELATIVE_NUMBER_OF_TESTCASES(NumberMetric.class), /** * The average number of assertions per testcase. */ - AVERAGE_ASSERTIONS_PER_TESTCASE, + AVERAGE_ASSERTIONS_PER_TESTCASE(NumberMetric.class), /** * The total complexity of the test cases divided by the complexity of the source code. */ - RELATIVE_CYCLOMATIC_COMPLEXITY, + RELATIVE_CYCLOMATIC_COMPLEXITY(NumberMetric.class), /** * Coupling between objects of the test cases divided by the coupling in the source code. */ - RELATIVE_COUPLING_BETWEEN_OBJECTS, + RELATIVE_COUPLING_BETWEEN_OBJECTS(NumberMetric.class), /** * The depth of the test cases divided by the depth of the source code. */ - RELATIVE_DEPTH_INHERITANCE_TREE, + RELATIVE_DEPTH_INHERITANCE_TREE(NumberMetric.class), /** * Test WMC divided by source code WMC. */ - RELATIVE_WEIGHT_METHOD_CLASS, + RELATIVE_WEIGHT_METHOD_CLASS(NumberMetric.class), /** * The ratio between test code and production code. */ - TEST_PRODUCTION_RATIO, + TEST_PRODUCTION_RATIO(NumberMetric.class), /** * The profanity checker metric. */ - PROFANITY, + PROFANITY(StringListMetric.class), /** * The list of failing tests. */ - TESTS, + TESTS(StringMetric.class), /** * Test coverage percentage. */ - TEST_COVERAGE, + TEST_COVERAGE(DoubleMetric.class), /** * Checkstyle complaints. */ - CHECKSTYLE, + CHECKSTYLE(Metric.class), /** * SpotBugs/FindBugs complaints. */ - SPOTBUGS; + SPOTBUGS(Metric.class); + + /** + * The type of the metric produced by instances with this name. + */ + private final Class<? extends Metric> type; + + /** + * Creates a new metric name. + * + * @param type the type of metric produces by instances with this name + */ + MetricName(final Class<? extends Metric> type) { + this.type = type; + } + + /** + * Returns the type of metric instances with this name accept. + * + * @return the type + */ + @Contract(pure = true) + public Class<? extends Metric> getType() { + return this.type; + } + + /** + * Checks whether the given metric instance is compatible with this metric name. + * + * @param instance the metric instance to check + * + * @return if the metric instance is {@code null} or if the instance is not compatible with + * the type of instances this metric name expects + */ + @Contract(value = "null -> false", pure = true) + public boolean isMetricCompatible(final @Nullable Metric<?> instance) { + return instance != null && this.getType().isAssignableFrom(instance.getClass()); + } /** * Converts the metric name to a name without underscores and in lower case. diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java index 3c5de3ec94c9a2fae37e5629758fbefc4f4f2fd6..e8755447f02473dc564bf0fa08391cbdc8e4fbe0 100644 --- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricSettings.java @@ -1,8 +1,13 @@ package nl.tudelft.ewi.auta.common.model.metric; +import nl.tudelft.ewi.auta.common.annotation.Unmodifiable; +import nl.tudelft.ewi.auta.common.model.PassingScriptTestCase; import org.intellij.lang.annotations.Language; import javax.annotation.Nullable; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -27,6 +32,11 @@ public class MetricSettings { @Nullable private String passingScript; + /** + * Unit tests for {@link #passingScript}. + */ + private Collection<PassingScriptTestCase> passingScriptTestCases = Collections.emptyList(); + /** * The JavaScript function to use to format the results. */ @@ -67,15 +77,7 @@ public class MetricSettings { final @Nullable Integer maxWarnings, final @Nullable Integer maxFailures ) { - this.metric = metric; - this.passingScript = passingScript; - this.formattingScript = formattingScript; - if (maxWarnings != null) { - this.maxWarnings = maxWarnings; - } - if (maxFailures != null) { - this.maxFailures = maxFailures; - } + this(metric, passingScript, formattingScript, maxWarnings, maxFailures, null); } /** @@ -126,9 +128,11 @@ public class MetricSettings { public MetricSettings(final MetricSettings other) { this.metric = other.metric; this.passingScript = other.passingScript; + this.passingScriptTestCases = List.copyOf(other.passingScriptTestCases); this.formattingScript = other.formattingScript; this.maxWarnings = other.maxWarnings; this.maxFailures = other.maxFailures; + this.dockerfile = other.dockerfile; } /** @@ -170,6 +174,25 @@ public class MetricSettings { this.passingScript = passingScript; } + /** + * Returns the unit tests for the metric's passing script. + * + * @return the unit tests + */ + @Unmodifiable + public Collection<PassingScriptTestCase> getPassingScriptTestCases() { + return this.passingScriptTestCases; + } + + /** + * Sets the unit tests for the metric's passing script. + * + * @param tests the unit tests + */ + public void setPassingScriptTestCases(final Collection<PassingScriptTestCase> tests) { + this.passingScriptTestCases = List.copyOf(tests); + } + /** * Returns the script used to format the results. * @@ -230,13 +253,15 @@ public class MetricSettings { && Objects.equals(this.formattingScript, other.formattingScript) && Objects.equals(this.maxFailures, other.maxFailures) && Objects.equals(this.maxWarnings, other.maxWarnings) - && Objects.equals(this.dockerfile, other.dockerfile); + && Objects.equals(this.dockerfile, other.dockerfile) + && Objects.equals(this.passingScriptTestCases, other.passingScriptTestCases); } @Override public final int hashCode() { return Objects.hash(this.metric, this.passingScript, this.formattingScript, - this.maxWarnings, this.maxFailures, this.dockerfile + this.maxWarnings, this.maxFailures, this.dockerfile, + this.passingScriptTestCases ); } } diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/NumberMetric.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/NumberMetric.java new file mode 100644 index 0000000000000000000000000000000000000000..f2e6d14a3fca176c8e4d2e56c406dfeccc650643 --- /dev/null +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/NumberMetric.java @@ -0,0 +1,18 @@ +package nl.tudelft.ewi.auta.common.model.metric; + +/** + * A type for metrics that contain single numbers. + * + * @param <T> the exact type of number stored + */ +public class NumberMetric<T extends Number> extends Metric<T> { + /** + * Creates a new number metric. + * + * @param value the value of the metric + * @param name the name of the metric + */ + public NumberMetric(final T value, final MetricName name) { + super(value, name); + } +} diff --git a/src/main/resources/nl/tudelft/ewi/auta/common/model/metric/test-fixtures.json b/src/main/resources/nl/tudelft/ewi/auta/common/model/metric/test-fixtures.json new file mode 100644 index 0000000000000000000000000000000000000000..a7165a9e3c9e8ec8c44548df06a1dbc0733721e6 --- /dev/null +++ b/src/main/resources/nl/tudelft/ewi/auta/common/model/metric/test-fixtures.json @@ -0,0 +1,428 @@ +{ + "fixtures": { + "ASSEMBLY_INSTRUCTION_COUNT": [ + 3, + 1461 + ], + "ASSEMBLY_RECURSION_TARGET": [ + [], + [ + "factorial" + ], + [ + "mult", + "main", + "factorial" + ] + ], + "ASSERTIONS_PER_TEST": [ + 0, + 1, + 3 + ], + "COMMENTED_LINE_COUNT": [ + 0, + 150 + ], + "CONSTRUCTOR_COUNT": [ + 0, + 1, + 3, + 14 + ], + "CYCLOMATIC_COMPLEXITY": [ + 1, + 4, + 9, + 12, + 16, + 759 + ], + "DOCKER_LOGS": [ + "BUILD FAILED", + "BUILD SUCCESS" + ], + "FIELD_COUNT": [ + 0, + 3, + 28 + ], + "FILE_COUNT": [ + 0, + 1, + 8, + 114 + ], + "JAVADOC_EXISTS": [ + false, + true + ], + "JAVADOC_VIOLATIONS": [ + [], + [ + "Missing @return statement" + ], + [ + "Missing @param tag for parameter arg", + "Missing @return statement" + ] + ], + "LINE_LENGTH": [ + [], + [ + 2, + 2 + ], + [ + 16, + 0, + 3, + 72, + 0, + 1 + ], + [ + 16, + 0, + 3, + 214, + 91, + 18, + 22, + 0 + ] + ], + "LINES_OF_CODE": [ + 0, + 13, + 182, + 638, + 21047 + ], + "METHOD_COUNT": [ + 0, + 2, + 38 + ], + "METHOD_EFFECTIVE_LOC": [ + 0, + 8, + 9131 + ], + "PARAMETER_COUNT": [ + 0, + 3, + 7, + 225 + ], + "PERCENTAGE_COMMENTED_LINES": [ + 0.0, + 0.23, + 0.79, + 1.0 + ], + "TEST_METHOD_COUNT": [ + 0, + 5, + 413 + ], + "MAINTAINABILITY_INDEX": [ + 0.0, + 0.08, + 0.13, + 0.61, + 1.0 + ], + "UNIQUE_WORDS_COUNT": [ + 5, + 1047 + ], + "LAMBDAS_COUNT": [ + 0, + 1, + 17 + ], + "SUBCLASSES_COUNT": [ + 0, + 1, + 12 + ], + "ANONYMOUS_CLASSES_COUNT": [ + 0, + 1, + 3, + 162 + ], + "MAX_NESTED_BLOCKS": [ + 0, + 2, + 59 + ], + "MATH_OPERATIONS_COUNT": [ + 0, + 1, + 10, + 12065 + ], + "ASSIGNMENTS_COUNT": [ + 0, + 4, + 721 + ], + "NUMBER_COUNT": [ + 0, + 2, + 132 + ], + "STRING_LITERAL_COUNT": [ + 0, + 6, + 613 + ], + "PARENTHESIZED_EXPRESSION_COUNT": [ + 0, + 1, + 75 + ], + "TRY_CATCH_COUNT": [ + 0, + 1, + 5, + 48 + ], + "COMPARISON_COUNT": [ + 0, + 1, + 90 + ], + "LOOP_COUNT": [ + 0, + 1, + 14 + ], + "VARIABLES_COUNT": [ + 0, + 3, + 53 + ], + "RETURN_COUNT": [ + 0, + 1, + 3, + 326 + ], + "WEIGHT_METHOD_CLASS": [ + 0, + 6, + 527 + ], + "RESPONSE_FOR_A_CLASS": [ + 0, + 12, + 1895 + ], + "COUPLING_BETWEEN_OBJECTS": [ + 0, + 4, + 37, + 172 + ], + "VARIABLES_USAGE": [ + {}, + { + "i": 14 + } + ], + "FIELD_USAGE": [ + {}, + { + "value": 3 + } + ], + "STATIC_METHOD_COUNT": [ + 0, + 1, + 9 + ], + "PUBLIC_METHOD_COUNT": [ + 0, + 1, + 8, + 37 + ], + "PRIVATE_METHOD_COUNT": [ + 0, + 1, + 5, + 47 + ], + "PROTECTED_METHOD_COUNT": [ + 0, + 1, + 3, + 39 + ], + "SYNCHRONIZED_METHOD_COUNT": [ + 0, + 1, + 20 + ], + "FINAL_METHOD_COUNT": [ + 0, + 1, + 7 + ], + "DEFAULT_METHOD_COUNT": [ + 0, + 1, + 3 + ], + "ABSTRACT_METHOD_COUNT": [ + 0, + 1, + 6 + ], + "PUBLIC_FIELD_COUNT": [ + 0, + 1, + 8, + 37 + ], + "PRIVATE_FIELD_COUNT": [ + 0, + 1, + 5, + 47 + ], + "PROTECTED_FIELD_COUNT": [ + 0, + 1, + 3, + 39 + ], + "SYNCHRONIZED_FIELD_COUNT": [ + 0, + 1, + 20 + ], + "FINAL_FIELD_COUNT": [ + 0, + 1, + 7 + ], + "DEFAULT_FIELD_COUNT": [ + 0, + 1, + 3 + ], + "ABSTRACT_FIELD_COUNT": [ + 0, + 1, + 6 + ], + "STATIC_FIELD_COUNT": [ + 0, + 1, + 9 + ], + "NUMBER_OF_STATIC_INVOCATIONS": [ + 0, + 3, + 27 + ], + "LACK_OF_COHESION_OF_METHODS": [ + 13, + 2, + 0 + ], + "DEPTH_INHERITANCE_TREE": [ + 0, + 1, + 29 + ], + "TEST_SMELLS": [ + [], + [ + "sleepy" + ] + ], + "PYLINT": [ + [] + ], + "DUPLICATE_CODE_BLOCKS": [ + [], + [ + { + "filenames": [], + "lineNumbers": [], + "lineCount": 3, + "tokenCount": 14, + "occurences": 2 + } + ] + ], + "SUPPRESSIONS": [ + [], + [ + { + "name": "TestClass", + "justification": "for testing" + } + ] + ], + "FILE_EFFECTIVE_LOC": [ + 2, + 38, + 2162 + ], + "RELATIVE_NUMBER_OF_ASSERTIONS": [ + 1, + 0.0, + 0.0021, + 0.0153 + ], + "RELATIVE_NUMBER_OF_TESTCASES": [ + 0.0, + 0.0019, + 0.0139 + ], + "AVERAGE_ASSERTIONS_PER_TESTCASE": [ + 0.893, + 1.027, + 2.103 + ], + "RELATIVE_CYCLOMATIC_COMPLEXITY": [ + 0.00174, + 0.0248, + 0.1659 + ], + "RELATIVE_COUPLING_BETWEEN_OBJECTS": [ + 0.0437, + 0.115, + 0.721 + ], + "RELATIVE_DEPTH_INHERITANCE_TREE": [ + 0.000274, + 0.00164, + 0.0512 + ], + "RELATIVE_WEIGHT_METHOD_CLASS": [ + 0.5 + ], + "TEST_PRODUCTION_RATIO": [ + 0.0, + 0.8, + 1.0, + 1.2 + ], + "PROFANITY": [ + [], + [ + "Line 6 contains profanity" + ] + ], + "TESTS": [ + "Assertion failure" + ] + } +} diff --git a/src/test/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCaseTest.java b/src/test/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b3615f100fc3e24ead5cf957ddbf880e5557f20f --- /dev/null +++ b/src/test/java/nl/tudelft/ewi/auta/common/model/PassingScriptTestCaseTest.java @@ -0,0 +1,14 @@ +package nl.tudelft.ewi.auta.common.model; + +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.jupiter.api.Test; + +public class PassingScriptTestCaseTest { + @Test + public void testEquals() { + EqualsVerifier.forClass(PassingScriptTestCase.class) + .suppress(Warning.NONFINAL_FIELDS) + .verify(); + } +} diff --git a/src/test/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProviderTest.java b/src/test/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProviderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f0d815e501c683636f74dc5c84647eba32893ded --- /dev/null +++ b/src/test/java/nl/tudelft/ewi/auta/common/model/metric/MetricFixturesProviderTest.java @@ -0,0 +1,18 @@ +package nl.tudelft.ewi.auta.common.model.metric; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MetricFixturesProviderTest { + @Test + public void testGetFixtures() { + final var provider = new MetricFixturesProvider(new Gson()); + final var fixtures = provider.getFixtures(); + + // Basic metrics that should have fixtures as a baseline + assertThat(fixtures) + .containsKeys(MetricName.CYCLOMATIC_COMPLEXITY, MetricName.LINES_OF_CODE); + } +} diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java b/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java index ec35bdb299192eff0af2877d4c76a45d1259dd37..04066d3e45d287bcdbe00c1e2a4a17db0e104bd6 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/worker/control/ControlProcessor.java @@ -200,6 +200,7 @@ public class ControlProcessor { metric.put("name", metricName); metric.put("internal_name", metricName); + metric.put("value_type", metricName.getType().getSimpleName()); metric.put("languages", analyzer.getLanguages()); metric.put("class", analyzer.getType() == Job.class ? "dynamic" : "static"); metric.put("script_presets", scriptPresets.get(metricName));