diff --git a/.auta.yml b/.auta.yml index 6a9c5f8a6f70a1a143541deaa3a8a17e70d2ddf8..e57980c18d209b195ec487a9b21083653dbc04a3 100644 --- a/.auta.yml +++ b/.auta.yml @@ -1,3 +1,9 @@ suppressions: - path: 'worker/src/test/resources/**' justification: Worker test resources are intentionally bad to test analyzers + +upload_suppressions: + - pattern: '.git' + justification: 'the git folder is part of version control and should be excluded' + - pattern: '.gitlab' + justification: 'this folder contains templates for issues, and is not relevant for code quality' \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index dcf1b1158d5f65e12806e9faf2e12af2623dee8b..1775f5d3085c443d4f5e09b6dd81fdf149812a1d 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -32,7 +32,7 @@ dependencies { implementation 'commons-fileupload:commons-fileupload:1.4' implementation 'org.apache.httpcomponents:httpclient:4.5.+' implementation 'org.apache.httpcomponents:httpmime:4.5.+' - implementation 'javax.validation:validation-api:2.0.1.Final' + implementation 'org.hibernate.validator:hibernate-validator:6.0.17.Final' implementation 'org.apache.commons:commons-csv:1.6' implementation project(':') diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CPMController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CPMController.java index 1cf72dbc3d2700a84a8af01840bd11bd0cfa1f48..31ab6ac541e3fdc1f59ee8116c9fa9108b49031a 100644 --- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CPMController.java +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CPMController.java @@ -6,6 +6,7 @@ import nl.tudelft.ewi.auta.core.database.IdentityContainer; import nl.tudelft.ewi.auta.core.database.Repositories; import nl.tudelft.ewi.auta.core.jobs.JobQueue; import nl.tudelft.ewi.auta.core.model.Assignment; +import nl.tudelft.ewi.auta.core.model.CPMDataModel; import nl.tudelft.ewi.auta.core.model.FileStore; import nl.tudelft.ewi.auta.core.model.Job; import nl.tudelft.ewi.auta.core.model.Submission; @@ -23,17 +24,19 @@ import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.StandardServletMultipartResolver; import javax.annotation.Nullable; +import javax.validation.Valid; import java.io.IOException; import java.io.OutputStream; import java.net.MalformedURLException; @@ -114,17 +117,14 @@ public class CPMController extends ControllerBase @RequestMapping(path = "/api/v1/cpm/{aid}", method = RequestMethod.POST) public ResponseEntity<Response> uploadAction( final @RequestParam(value = "file") MultipartFile file, - final @RequestParam(value = "projectName") String projectName, - final @RequestParam(value = "workunitID") String workunitID, - final @RequestPart(value = "workunitName") String workunitName, - final @RequestPart(value = "memberName") String memberName, - final @RequestPart(value = "uploadComments", required = false) String uploadComments, - final @RequestParam(value = "verificationCode") String verificationCode, - final @RequestPart(value = "filename") String filename, - final @PathVariable String aid - ) throws IOException { + final @PathVariable String aid, + final @Valid @ModelAttribute CPMDataModel model, + final BindingResult result) throws IOException { + this.checkForErrors(result); var res = new Response(); + final var filename = model.getFilename(); + final var assignmentRepository = this.repositories.getAssignmentRepository(); final var submissionRepository = this.repositories.getSubmissionRepository(); @@ -134,26 +134,30 @@ public class CPMController extends ControllerBase this.files.add(filename, temp); final var assignmentOptional = assignmentRepository.findById(aid); - if (!assignmentOptional.isPresent()) { + if (assignmentOptional.isEmpty()) { res.addError(ErrorCode.NO_SUCH_ASSIGNMENT, "There is no assignment with ID " + aid); return this.notFound().body(res); } var assignment = assignmentOptional.get(); - assignment = this.addCPMdetails(projectName, workunitID, workunitName, assignment); + this.addCPMDetails(model, assignment); assignment = assignmentRepository.save(assignment); - var submission = new Submission(projectName + ":" + workunitName, + var submission = new Submission(model.getSubmissionName(), filename, aid); - submission.setCpmVerificationCode(verificationCode); + submission.setCpmVerificationCode(model.getVerificationCode()); submission.setAssignmentId(aid); submission.getPipelineLog().setSubmitted(Instant.now()); submission.setContents(filename); submission = submissionRepository.save(submission); + // Once saved in the repository, the id should never be null. + assert submission.getId() != null; + final var sid = submission.getId(); - var sid = submission.getId(); logger.debug("Created new submission for CPM submission, submission id {}", sid); + final var memberName = model.getMemberName(); + this.repositories.getIdentityRepository().save(new IdentityContainer(sid, memberName)); logger.debug("CPM submission {} belongs to {}", sid, memberName); @@ -218,27 +222,19 @@ public class CPMController extends ControllerBase } /** - * Adds the project name, workunit id and name to an assignment. If any of these values are - * null in an assignment, update all three of the values. - * @param projectName the project name to add to the assignment - * @param workUnitID the workunit id to add to the assignment - * @param workUnitName the workunit name to add - * @param assignment the assignment everything will be added to - * @return the updated assignment + * Adds project name, workunitId and workunitName to an assignment. + * + * @param model the model to get details from. + * @param assignment the assignment to add details to. */ - private Assignment addCPMdetails(final String projectName, final String workUnitID, - final String workUnitName, final Assignment assignment) { - if (assignment.getProjectName() == null - || assignment.getWorkUnitId() == null - || assignment.getWorkUnitName() == null) { - - assignment.setProjectName(projectName); - assignment.setWorkUnitId(workUnitID); - assignment.setWorkUnitName(workUnitName); - } - return this.repositories.getAssignmentRepository().save(assignment); + private void addCPMDetails(final CPMDataModel model, final Assignment assignment) { + assignment.setProjectName(model.getProjectName()); + assignment.setWorkUnitId(model.getWorkunitID()); + assignment.setWorkUnitName(model.getWorkunitName()); + } + /** * Gets the extension of a filename * @param filename the filename 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 09d5a49edc5c31712a0476bfa4c6ff517e4ddfed..d71ff1e8be4baab062640e263607ee7c7d422352 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 @@ -1,10 +1,13 @@ package nl.tudelft.ewi.auta.core.controller; import java.util.Map; +import java.util.Objects; import nl.tudelft.ewi.auta.core.response.exception.FieldTypeMismatchException; import nl.tudelft.ewi.auta.core.response.exception.MissingFieldException; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; /** * Provides utility functions to controllers. @@ -65,6 +68,24 @@ public class ControllerBase { return this.getParam(req, key, Map.class); } + /** + * Checks if there are any results, and throws a field type mismatch if there are any. + * + * @param result the results of the validation. + * @throws FieldTypeMismatchException if there are any errors + */ + protected void checkForErrors(final BindingResult result) { + if (result.hasErrors()) { + final var error = result.getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(FieldTypeMismatchException::new); + throw new FieldTypeMismatchException(error); + } + } + + /** * Returns a builder for a Not Found response entity. * diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CourseController.java b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CourseController.java index ed1186fc1ca754bedaf06146364583fbc3e550c5..dd2c7c1472e78f9d59e406a07e4afbd903a89e60 100644 --- a/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CourseController.java +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/controller/CourseController.java @@ -6,14 +6,12 @@ import nl.tudelft.ewi.auta.core.model.Course; import nl.tudelft.ewi.auta.core.model.CourseValidator; import nl.tudelft.ewi.auta.core.response.Response; import nl.tudelft.ewi.auta.core.response.exception.CourseAlreadyExistsException; -import nl.tudelft.ewi.auta.core.response.exception.FieldTypeMismatchException; import nl.tudelft.ewi.auta.core.response.exception.InvalidRoleException; import nl.tudelft.ewi.auta.core.response.exception.MisconfiguredUserException; import nl.tudelft.ewi.auta.core.response.exception.MissingUserException; import nl.tudelft.ewi.auta.core.response.exception.UserNotAuthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.User; import org.springframework.stereotype.Controller; @@ -31,7 +29,6 @@ import org.springframework.web.bind.annotation.RequestBody; import java.sql.SQLException; import java.util.List; import java.util.Map; -import java.util.Objects; /** @@ -343,23 +340,4 @@ public class CourseController extends ControllerBase { } return this.securedService.addTA(course, username); } - - - - /** - * Checks if there are any results, and throws a field type mismatch if there are any. - * - * @param result the results of the validation. - * @throws FieldTypeMismatchException if there are any errors - */ - private void checkForErrors(final BindingResult result) { - if (result.hasErrors()) { - final var error = result.getAllErrors().stream() - .map(DefaultMessageSourceResolvable::getDefaultMessage) - .filter(Objects::nonNull) - .findFirst() - .orElseThrow(FieldTypeMismatchException::new); - throw new FieldTypeMismatchException(error); - } - } } diff --git a/core/src/main/java/nl/tudelft/ewi/auta/core/model/CPMDataModel.java b/core/src/main/java/nl/tudelft/ewi/auta/core/model/CPMDataModel.java new file mode 100644 index 0000000000000000000000000000000000000000..88d14ce07b068212c47d606a1ceb3bb2866c18a5 --- /dev/null +++ b/core/src/main/java/nl/tudelft/ewi/auta/core/model/CPMDataModel.java @@ -0,0 +1,190 @@ +package nl.tudelft.ewi.auta.core.model; + +import javax.annotation.Nullable; +import javax.validation.constraints.NotBlank; + +/** + * The data model used by CPM. + */ +public class CPMDataModel { + // Annotating the fields with @NotBlank and initializing them to an empty string is not a + // mistake, hibernate validation checks if they are not blank after a request has been + // received. + + /** + * The project name. + */ + @NotBlank + private String projectName = ""; + + /** + * The workunit id. + */ + @NotBlank + private String workunitID = ""; + + /** + * The workunit name. + */ + @NotBlank + private String workunitName = ""; + + /** + * The member name. + */ + @NotBlank + private String memberName = ""; + + /** + * Upload comments associated with the submission. + */ + @Nullable + private String uploadComments; + + /** + * The verification code. + */ + @NotBlank + private String verificationCode = ""; + + /** + * The filename. + */ + @NotBlank + private String filename = ""; + + /** + * Gets the project name. + * + * @return the project name + */ + public String getProjectName() { + return this.projectName; + } + + /** + * Sets the projectName. + * + * @param projectName the projectName to set + */ + public void setProjectName(final String projectName) { + this.projectName = projectName; + } + + /** + * Gets the workunit ID. + * + * @return the workunit id + */ + public String getWorkunitID() { + return this.workunitID; + } + + /** + * Sets the workunitID. + * + * @param workunitID the workunitID to set + */ + public void setWorkunitID(final String workunitID) { + this.workunitID = workunitID; + } + + /** + * Gets the workunit name. + * + * @return the workunit name + */ + public String getWorkunitName() { + return this.workunitName; + } + + /** + * Sets the workunitName. + * + * @param workunitName the workunitName to set + */ + public void setWorkunitName(final String workunitName) { + this.workunitName = workunitName; + } + + /** + * Gets the member name. + * + * @return the member name + */ + public String getMemberName() { + return this.memberName; + } + + /** + * Sets the memberName. + * + * @param memberName the memberName to set + */ + public void setMemberName(final String memberName) { + this.memberName = memberName; + } + + /** + * Gets the upload comments. + * + * @return the upload comments + */ + @Nullable + public String getUploadComments() { + return this.uploadComments; + } + + /** + * Sets the uploadComments. + * + * @param uploadComments the uploadComments to set + */ + public void setUploadComments(final @Nullable String uploadComments) { + this.uploadComments = uploadComments; + } + + /** + * Gets the verification code. + * + * @return the verification code + */ + public String getVerificationCode() { + return this.verificationCode; + } + + /** + * Sets the verificationCode. + * + * @param verificationCode the verificationCode to set + */ + public void setVerificationCode(final String verificationCode) { + this.verificationCode = verificationCode; + } + + /** + * Gets the filename. + * + * @return the filename + */ + public String getFilename() { + return this.filename; + } + + /** + * Sets the filename. + * + * @param filename the filename to set + */ + public void setFilename(final String filename) { + this.filename = filename; + } + + /** + * Gets the name for a CPM submission. + * @return the submission name. + */ + public String getSubmissionName() { + return String.format("%s:%s", this.projectName, this.workunitName); + } +} diff --git a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/CPMControllerTest.java b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/CPMControllerTest.java index 160076062ce78adab736556e1f6a56bea44f533d..a8a6967c2cb9817b06fba343a683c606a4d39ad5 100644 --- a/core/src/test/java/nl/tudelft/ewi/auta/core/controller/CPMControllerTest.java +++ b/core/src/test/java/nl/tudelft/ewi/auta/core/controller/CPMControllerTest.java @@ -11,6 +11,7 @@ import nl.tudelft.ewi.auta.core.database.Repositories; import nl.tudelft.ewi.auta.core.database.SubmissionRepository; import nl.tudelft.ewi.auta.core.jobs.JobQueue; import nl.tudelft.ewi.auta.core.model.Assignment; +import nl.tudelft.ewi.auta.core.model.CPMDataModel; import nl.tudelft.ewi.auta.core.model.FileStore; import nl.tudelft.ewi.auta.core.model.Submission; import nl.tudelft.ewi.auta.core.report.CpmReportGenerator; @@ -26,9 +27,11 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.validation.BindingResult; import java.io.IOException; import java.net.URL; @@ -67,6 +70,8 @@ public class CPMControllerTest { @Mock private EntityRepository entityRepository; + @Mock + private BindingResult bindingResult; @Mock private Repositories repositories; @@ -91,6 +96,8 @@ public class CPMControllerTest { private static final String CPM_URL = "http://auta.f00f.nl/"; + private CPMDataModel model; + @InjectMocks private CPMController controller; @@ -125,7 +132,7 @@ public class CPMControllerTest { MockitoAnnotations.initMocks(this); Mockito.when(this.generator.generateReport(Mockito.any())).thenReturn("report"); - + Mockito.when(this.bindingResult.hasErrors()).thenReturn(false); Mockito.when(this.repositories.getAssignmentRepository()).thenReturn(this.assignmentStore); Mockito.when(this.repositories.getSubmissionRepository()).thenReturn(this.submissionStore); Mockito.when(this.repositories.getIdentityRepository()).thenReturn(this.identityRepository); @@ -136,7 +143,7 @@ public class CPMControllerTest { assignment1.setId(AID); return assignment1; }; - + this.model = this.initializeModel(); Mockito.when(this.entityRepository.findByParentIds(eq(SID), eq(AID))).thenReturn( Optional.of(this.container)); @@ -163,126 +170,53 @@ public class CPMControllerTest { Mockito.when(this.settings.get(Mockito.any())).thenReturn(CPM_URL); Mockito.when(this.settings.get(Mockito.any(), Mockito.any())).thenReturn(CPM_URL); - - this.mvc = MockMvcBuilders.standaloneSetup(this.controller).build(); } - @Test - public void cpmRequestTest() throws IOException { - var newSubmission = new Submission(PROJECT_NAME + ":" + WORKUNIT_NAME, FILENAME, AID); - newSubmission.setCpmVerificationCode(VERIFICATION_CODE); - newSubmission.setAssignmentId(AID); - newSubmission.setId(SID); - - var multipartFile = new MockMultipartFile("filename", "contents".getBytes()); - this.controller.uploadAction(multipartFile, - PROJECT_NAME, - WORKUNIT_ID, - WORKUNIT_NAME, - MEMBER_NAME, - "comments", - VERIFICATION_CODE, - FILENAME, - AID); - Mockito.verify(this.assignmentStore, Mockito.atLeastOnce()).findById(eq(AID)); - Mockito.verify(this.submissionStore, Mockito.atLeastOnce()) - .save(Mockito.argThat(new SubmissionMatcher(newSubmission))); + private CPMDataModel initializeModel() { + final var newModel = new CPMDataModel(); + newModel.setProjectName(PROJECT_NAME); + newModel.setWorkunitID(WORKUNIT_ID); + newModel.setWorkunitName(WORKUNIT_NAME); + newModel.setUploadComments("comments"); + newModel.setVerificationCode(VERIFICATION_CODE); + newModel.setFilename(FILENAME); + return newModel; } @Test - public void cpmRequestEmptyFilenames() throws IOException { + public void cpmRequestTest() throws IOException { var newSubmission = new Submission(PROJECT_NAME + ":" + WORKUNIT_NAME, FILENAME, AID); newSubmission.setCpmVerificationCode(VERIFICATION_CODE); newSubmission.setAssignmentId(AID); newSubmission.setId(SID); - var multipartFile = new MockMultipartFile("filename", "contents".getBytes()); - this.controller.uploadAction(multipartFile, - PROJECT_NAME, - WORKUNIT_ID, - WORKUNIT_NAME, - MEMBER_NAME, - "comments", - VERIFICATION_CODE, - "", - AID); - Mockito.verify(this.assignmentStore, Mockito.atLeastOnce()).findById(eq(AID)); - Mockito.verify(this.fileStore, Mockito.atLeastOnce()).add(eq(""), - Mockito.any()); - Mockito.verify(this.submissionStore, Mockito.atLeastOnce()) - .save(Mockito.argThat(new SubmissionMatcher(newSubmission))); - } + var newAssignment = new Assignment("ASSIGNMENT_NAME"); + newAssignment.setId(AID); + newAssignment.setWorkUnitName(WORKUNIT_NAME); + newAssignment.setProjectName(PROJECT_NAME); + newAssignment.setWorkUnitId(WORKUNIT_ID); - @Test - public void cpmRequestInvalidFilenames() throws IOException { - var newSubmission = new Submission(PROJECT_NAME + ":" + WORKUNIT_NAME, FILENAME, AID); - newSubmission.setCpmVerificationCode(VERIFICATION_CODE); - newSubmission.setAssignmentId(AID); - newSubmission.setId(SID); + Mockito.when(this.assignmentStore.findById(eq(AID))).thenReturn( + Optional.of(this.assignment)); var multipartFile = new MockMultipartFile("filename", "contents".getBytes()); this.controller.uploadAction(multipartFile, - PROJECT_NAME, - WORKUNIT_ID, - WORKUNIT_NAME, - MEMBER_NAME, - "comments", - VERIFICATION_CODE, - "/awda/awdacv/.zip.zip.java.", - AID); + AID, this.model, this.bindingResult); Mockito.verify(this.assignmentStore, Mockito.atLeastOnce()).findById(eq(AID)); - Mockito.verify(this.fileStore, Mockito.atLeastOnce()).add( - eq("/awda/awdacv/.zip.zip.java."), Mockito.any()); + Mockito.verify(this.assignmentStore, Mockito.atLeastOnce()) + .save(Mockito.argThat(new AssignmentMatcher(newAssignment))); Mockito.verify(this.submissionStore, Mockito.atLeastOnce()) .save(Mockito.argThat(new SubmissionMatcher(newSubmission))); } - @Test public void cpmInvalidAidTest() throws IOException { var multipartFile = new MockMultipartFile("filename", "contents".getBytes()); - var response = this.controller.uploadAction(multipartFile, - PROJECT_NAME, - WORKUNIT_ID, - WORKUNIT_NAME, - MEMBER_NAME, - "comments", - VERIFICATION_CODE, - FILENAME, - "invalid_aid"); - - assertThat(response.getStatusCodeValue()).isEqualTo(404); - } - - @Test - public void cpmRequestAssignmentWithoutCPMDetailsTest() throws IOException { - this.assignment = new Assignment("ASSIGNMENT_NAME"); - this.assignment.setId(AID); - - Mockito.when(this.assignmentStore.findById(eq(AID))).thenReturn( - Optional.of(this.assignment)); - - var multipartFile = new MockMultipartFile("filename", "contents".getBytes()); - this.controller.uploadAction(multipartFile, - PROJECT_NAME, - WORKUNIT_ID, - WORKUNIT_NAME, - MEMBER_NAME, - "comments", - VERIFICATION_CODE, - FILENAME, - AID); + var response = this.controller.uploadAction(multipartFile, "INVALID_AID", this.model, + this.bindingResult); - var newAssignment = new Assignment("ASSIGNMENT_NAME"); - newAssignment.setId(AID); - newAssignment.setWorkUnitName(WORKUNIT_NAME); - newAssignment.setProjectName(PROJECT_NAME); - newAssignment.setWorkUnitId(WORKUNIT_ID); - - Mockito.verify(this.assignmentStore, Mockito.atLeastOnce()).findById(eq(AID)); - Mockito.verify(this.assignmentStore, Mockito.atLeastOnce()) - .save(Mockito.argThat(new AssignmentMatcher(newAssignment))); + assertThat(response.getStatusCodeValue()).isEqualTo(HttpStatus.NOT_FOUND.value()); } public class AssignmentMatcher implements ArgumentMatcher<Assignment> { diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/PyLintResult.java b/src/main/java/nl/tudelft/ewi/auta/common/model/PyLintResult.java index 7c851cf1dc230dd282b28e5c3d13e8d477b18dbf..3a5363bbc47fbf293b77a57e73408e358e69b2cf 100644 --- a/src/main/java/nl/tudelft/ewi/auta/common/model/PyLintResult.java +++ b/src/main/java/nl/tudelft/ewi/auta/common/model/PyLintResult.java @@ -50,6 +50,22 @@ public class PyLintResult { */ private String messageid; + /** + * Creates a new empty PyLintResult. + */ + public PyLintResult() { + this.type = ""; + this.module = ""; + this.obj = ""; + this.line = ""; + this.column = ""; + this.path = ""; + this.symbol = ""; + this.message = ""; + this.messageid = ""; + } + + /** * Retrieves the type of the feedback. * @return the feedback type @@ -123,28 +139,83 @@ public class PyLintResult { } /** - * Creates a new PyLintResult. - * @param type the type of the feedback - * @param module the module of the feedback - * @param obj the object of the feedback - * @param line the line of the feedback - * @param column the column of the feedback - * @param path the path of the feedback - * @param symbol the symbol of the feedback - * @param message the message of the feedback - * @param messageid the message id of the feedback + * Sets the type. + * + * @param type the type to set */ - public PyLintResult(final String type, final String module, final String obj, - final String line, final String column, final String path, - final String symbol, final String message, final String messageid) { + public void setType(final String type) { this.type = type; + } + + /** + * Sets the module name the feedback is from. + * + * @param module the module to set + */ + public void setModule(final String module) { this.module = module; + } + + /** + * Sets the class and/or method/function the feedback is from.. + * + * @param obj class, method or function the feedback is from + */ + public void setObj(final String obj) { this.obj = obj; + } + + /** + * Sets the line the feedback refers to. + * + * @param line the line to set + */ + public void setLine(final String line) { this.line = line; + } + + /** + * Sets the column the feedback refers to. + * + * @param column the column to set + */ + public void setColumn(final String column) { this.column = column; + } + + /** + * Sets the path to the file the feedback was generated from. + * + * @param path the path to set + */ + public void setPath(final String path) { this.path = path; + } + + /** + * Sets the symbol the feedback is saved under. + * + * @param symbol the symbol to set + */ + public void setSymbol(final String symbol) { this.symbol = symbol; + } + + /** + * Sets the message that is associated with the feedback. + * + * @param message the message to set + */ + public void setMessage(final String message) { this.message = message; + } + + /** + * Sets the id of the message the feedback refers to. + * + * @param messageid the messageid to set + */ + public void setMessageid(final String messageid) { this.messageid = messageid; } } diff --git a/src/test/java/nl/tudelft/ewi/auta/common/model/PyLintResultTest.java b/src/test/java/nl/tudelft/ewi/auta/common/model/PyLintResultTest.java index 202897067f84639abc8434b37cf032db40222fea..deb830e778f6f3b1fc87eba34cab03ce04016d00 100644 --- a/src/test/java/nl/tudelft/ewi/auta/common/model/PyLintResultTest.java +++ b/src/test/java/nl/tudelft/ewi/auta/common/model/PyLintResultTest.java @@ -8,8 +8,16 @@ public class PyLintResultTest { @Test public void testConstructor() { - final var result = new PyLintResult("type", "module", "obj", "line", - "column", "path", "symbol", "message", "messageid"); + final var result = new PyLintResult(); + result.setType("type"); + result.setModule("module"); + result.setObj("obj"); + result.setLine("line"); + result.setColumn("column"); + result.setPath("path"); + result.setSymbol("symbol"); + result.setMessage("message"); + result.setMessageid("messageid"); assertThat(result.getType()).isEqualTo("type"); assertThat(result.getModule()).isEqualTo("module"); assertThat(result.getObject()).isEqualTo("obj"); diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/JobAnalyzer.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/JobAnalyzer.java index 8ab02d41fa467a991541f9fe26981d070de51b62..b26994a00ace7dab24042c93213f210770834318 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/JobAnalyzer.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/JobAnalyzer.java @@ -2,6 +2,9 @@ package nl.tudelft.ewi.auta.checker; import nl.tudelft.ewi.auta.worker.Job; +import java.util.List; +import java.util.stream.Collectors; + /** * An analyzer for submissions. */ @@ -10,4 +13,16 @@ public abstract class JobAnalyzer implements Analyzer<Job> { public Class<Job> getType() { return Job.class; } + + /** + * Gets a list of all filenames. + * + * @param job the job to get the filenames from + * @return a list of filenames + */ + public List<String> getFileNames(final Job job) { + return job.getProject().getFiles().stream() + .map(file -> file.getAbsolutePath().toString()) + .collect(Collectors.toList()); + } } diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/CPDChecker.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/CPDChecker.java index 1da1b158c37448efe0e522235d5888d6fda5559f..a5fdfaba8da593a11caa76645527aaf3997fdf18 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/CPDChecker.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/CPDChecker.java @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; /** * A checker that uses the CPD tool (Copy Paste Detector) included in the PMD suite to find @@ -72,9 +71,7 @@ public class CPDChecker extends JobAnalyzer { //Creates a file that contains a list of paths to files that are to be analyzed by CPD. var fileListPath = Files.createTempFile(this.workerSettings.getTemp(), "stream", ".csv"); - var fragmentPaths = victim.getProject().getFiles().stream() - .map(f -> f.getAbsolutePath().toString()) - .collect(Collectors.toList()); + var fragmentPaths = this.getFileNames(victim); try (var fileWriter = new FileWriter(fileListPath.toFile(), StandardCharsets.UTF_8)) { fileWriter.write(String.join(",", fragmentPaths)); } diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/Lizard.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/Lizard.java index 7ba43c256304053807eb90b50b093342aede8042..f277a850d50dc89b86a9cf72d2db0681ec68a86d 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/Lizard.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/generic/Lizard.java @@ -104,7 +104,7 @@ public class Lizard extends EntityAnalyzer { /** * A pattern matching paamayim nekudotayim. */ - private static final Pattern PAAMAYIM_NEKUDOTAYIM = Pattern.compile("::"); + private static final Pattern DOUBLE_COLON = Pattern.compile("::"); /** * The index of LOC metrics in the XML. @@ -298,7 +298,7 @@ public class Lizard extends EntityAnalyzer { } final var sep = NAMESPACE_SEPARATORS.getOrDefault(language, DEFAULT_SEPARATOR); - final var namespace = PAAMAYIM_NEKUDOTAYIM + final var namespace = DOUBLE_COLON .matcher(this.getAttribute(item, "namespace")) .replaceAll(sep); final var numParameters = Integer.parseInt(this.getAttribute(item, "param-count")); diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/CKAnalyzer.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/CKAnalyzer.java index 83a263d4b9d0d8ae5e46bcdae3544f55297a290a..a92afd2c8200a73a418e67cce92bbc9b57294487 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/CKAnalyzer.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/CKAnalyzer.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; public class CKAnalyzer extends EntityAnalyzer { @@ -176,67 +177,68 @@ public class CKAnalyzer extends EntityAnalyzer { private Entity getClassLevelMetrics(final CKClassResult rc, final Entity fileEntity) { final var ce = new Entity(fileEntity, rc.getClassName().substring( rc.getClassName().lastIndexOf(".") + 1), false, EntityLevel.CLASS); - ce.addMetric(new IntegerMetric(rc.getReturnQty(), MetricName.RETURN_COUNT)); - ce.addMetric(new IntegerMetric(rc.getLoopQty(), MetricName.LOOP_COUNT)); - ce.addMetric(new IntegerMetric(rc.getComparisonsQty(), MetricName.COMPARISON_COUNT)); - ce.addMetric(new IntegerMetric(rc.getTryCatchQty(), MetricName.TRY_CATCH_COUNT)); - ce.addMetric(new IntegerMetric(rc.getParenthesizedExpsQty(), - MetricName.PARENTHESIZED_EXPRESSION_COUNT)); - ce.addMetric(new IntegerMetric(rc.getStringLiteralsQty(), MetricName.STRING_LITERAL_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumbersQty(), MetricName.NUMBER_COUNT)); - ce.addMetric(new IntegerMetric(rc.getAssignmentsQty(), MetricName.ASSIGNMENTS_COUNT)); - ce.addMetric(new IntegerMetric(rc.getMathOperationsQty(), - MetricName.MATH_OPERATIONS_COUNT)); - ce.addMetric(new IntegerMetric(rc.getVariablesQty(), MetricName.VARIABLES_COUNT)); - ce.addMetric(new IntegerMetric(rc.getMaxNestedBlocks(), MetricName.MAX_NESTED_BLOCKS)); - ce.addMetric(new IntegerMetric(rc.getNumberOfAbstractMethods(), - MetricName.ANONYMOUS_CLASSES_COUNT)); - ce.addMetric(new IntegerMetric(rc.getSubClassesQty(), MetricName.SUBCLASSES_COUNT)); - ce.addMetric(new IntegerMetric(rc.getLambdasQty(), MetricName.LAMBDAS_COUNT)); - ce.addMetric(new IntegerMetric(rc.getUniqueWordsQty(), MetricName.UNIQUE_WORDS_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfMethods(), MetricName.METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfStaticMethods(), - MetricName.STATIC_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfPublicMethods(), - MetricName.PUBLIC_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfPrivateMethods(), - MetricName.PRIVATE_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfProtectedMethods(), - MetricName.PROTECTED_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfDefaultMethods(), - MetricName.DEFAULT_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfAbstractMethods(), - MetricName.ABSTRACT_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfFinalMethods(), - MetricName.FINAL_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfSynchronizedMethods(), - MetricName.SYNCHRONIZED_METHOD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfFields(), MetricName.FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfStaticFields(), - MetricName.STATIC_FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfPublicFields(), - MetricName.PUBLIC_FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfPrivateFields(), - MetricName.PRIVATE_FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfProtectedFields(), - MetricName.PROTECTED_FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfDefaultFields(), - MetricName.DEFAULT_FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfFinalFields(), MetricName.FINAL_FIELD_COUNT)); - ce.addMetric(new IntegerMetric(rc.getNumberOfSynchronizedFields(), - MetricName.SYNCHRONIZED_FIELD_COUNT)); + this.addIntegerMetric(ce, rc::getReturnQty, MetricName.RETURN_COUNT); + this.addIntegerMetric(ce, rc::getLoopQty, MetricName.LOOP_COUNT); + this.addIntegerMetric(ce, rc::getComparisonsQty, MetricName.COMPARISON_COUNT); + this.addIntegerMetric(ce, rc::getTryCatchQty, MetricName.TRY_CATCH_COUNT); + this.addIntegerMetric(ce, rc::getStringLiteralsQty, MetricName.STRING_LITERAL_COUNT); + this.addIntegerMetric(ce, rc::getNumbersQty, MetricName.NUMBER_COUNT); + this.addIntegerMetric(ce, rc::getAssignmentsQty, MetricName.ASSIGNMENTS_COUNT); + this.addIntegerMetric(ce, rc::getMathOperationsQty, MetricName.MATH_OPERATIONS_COUNT); + this.addIntegerMetric(ce, rc::getVariablesQty, MetricName.VARIABLES_COUNT); + this.addIntegerMetric(ce, rc::getMaxNestedBlocks, MetricName.MAX_NESTED_BLOCKS); + this.addIntegerMetric(ce, rc::getSubClassesQty, MetricName.SUBCLASSES_COUNT); + this.addIntegerMetric(ce, rc::getLambdasQty, MetricName.LAMBDAS_COUNT); + this.addIntegerMetric(ce, rc::getUniqueWordsQty, MetricName.UNIQUE_WORDS_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfMethods, MetricName.METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfStaticMethods, MetricName.STATIC_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfPublicMethods, MetricName.PUBLIC_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfPrivateMethods, MetricName.PRIVATE_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfDefaultMethods, MetricName.DEFAULT_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfAbstractMethods, MetricName.ABSTRACT_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfFinalMethods, MetricName.FINAL_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfFields, MetricName.FIELD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfStaticFields, MetricName.STATIC_FIELD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfPublicFields, MetricName.PUBLIC_FIELD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfPrivateFields, MetricName.PRIVATE_FIELD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfProtectedFields, MetricName.PROTECTED_FIELD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfDefaultFields, MetricName.DEFAULT_FIELD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfFinalFields, MetricName.FINAL_FIELD_COUNT); - ce.addMetric(new IntegerMetric(rc.getLoc(), MetricName.LINES_OF_CODE)); - ce.addMetric(new IntegerMetric(rc.getNosi(), MetricName.NUMBER_OF_STATIC_INVOCATIONS)); - ce.addMetric(new IntegerMetric(rc.getRfc(), MetricName.RESPONSE_FOR_A_CLASS)); - ce.addMetric(new IntegerMetric(rc.getLcom(), MetricName.LACK_OF_COHESION_OF_METHODS)); - ce.addMetric(new IntegerMetric(rc.getCbo(), MetricName.COUPLING_BETWEEN_OBJECTS)); - ce.addMetric(new IntegerMetric(rc.getWmc(), MetricName.WEIGHT_METHOD_CLASS)); - ce.addMetric(new IntegerMetric(rc.getDit(), MetricName.DEPTH_INHERITANCE_TREE)); + this.addIntegerMetric(ce, rc::getParenthesizedExpsQty, + MetricName.PARENTHESIZED_EXPRESSION_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfAbstractMethods, + MetricName.ANONYMOUS_CLASSES_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfProtectedMethods, + MetricName.PROTECTED_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfSynchronizedMethods, + MetricName.SYNCHRONIZED_METHOD_COUNT); + this.addIntegerMetric(ce, rc::getNumberOfSynchronizedFields, + MetricName.SYNCHRONIZED_FIELD_COUNT); + + this.addIntegerMetric(ce, rc::getLoc, MetricName.LINES_OF_CODE); + this.addIntegerMetric(ce, rc::getNosi, MetricName.NUMBER_OF_STATIC_INVOCATIONS); + this.addIntegerMetric(ce, rc::getRfc, MetricName.RESPONSE_FOR_A_CLASS); + this.addIntegerMetric(ce, rc::getLcom, MetricName.LACK_OF_COHESION_OF_METHODS); + this.addIntegerMetric(ce, rc::getCbo, MetricName.COUPLING_BETWEEN_OBJECTS); + this.addIntegerMetric(ce, rc::getWmc, MetricName.WEIGHT_METHOD_CLASS); + this.addIntegerMetric(ce, rc::getDit, MetricName.DEPTH_INHERITANCE_TREE); return ce; } + /** + * Adds an integer metric to an entity. + * + * @param entity the entity to add the metric to + * @param supplier the function that will be called that supplies an integer + * @param metricName the metric name + */ + private void addIntegerMetric(final Entity entity, final Supplier<Integer> supplier, + final MetricName metricName) { + entity.addMetric(new IntegerMetric(supplier.get(), metricName)); + } + /** * Creates a mapping of the method name to method level metrics. * @param resultClass the class that contains the results diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/ClassAndMethodAnalyticsJava.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/ClassAndMethodAnalyticsJava.java index b1470ade8501e12c613f89f3e87c9e3d67275dab..0128eb6e6445982f8ca6d5e6c030a130c4c51da3 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/ClassAndMethodAnalyticsJava.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/java/ClassAndMethodAnalyticsJava.java @@ -187,7 +187,13 @@ public class ClassAndMethodAnalyticsJava extends EntityAnalyzer { )); map.put(MetricName.PARAMETER_COUNT, Collections.singletonList( - this.mapIntWarnFailMax("has many parameters", "has too many parameters", 8, 12) + this.numberWarnFailMax("has many parameters", "has too many parameters", 7, 10, + "A high parameter count often means that the method contains too much " + + "logic. This can be solved by either splitting the method and " + + "thus reducing the amount of parameters or creating a new class " + + "which has some of the parameters in it and than simply pass " + + "that class as parameter." + ) )); return map; diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java index f86b2686949de15c348d5a40c410a05af2e119f0..b40392180b53a7f717b0f8445248e071ee11d94c 100644 --- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLint.java @@ -20,11 +20,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -98,17 +95,8 @@ public class PyLint extends JobAnalyzer { justification = "SpotBugs is not aware of try-with-resources and its null checks" ) public void analyze(final Job victim, final Object options) throws IOException { - final var analyzePath = victim.getDir().toAbsolutePath(); - //final var fileList = analyzePath.toFile().listFiles(); - - final var fileNames = new ArrayList<String>(); - try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream( - Paths.get(analyzePath.toAbsolutePath().toString()))) { - for (Path path : directoryStream) { - fileNames.add(path.toString()); - } - } + final var fileNames = this.getFileNames(victim); // For every module for (var i = 0; i < fileNames.size(); i++) { @@ -146,7 +134,9 @@ public class PyLint extends JobAnalyzer { // Retrieve the JSon object from the process final var pyLintResults = new HashSet<PyLintResult>(); - var gson = new GsonBuilder().create(); + var gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(PyLintResult.class, new PyLintOutputDeserializer()); + final var gson = gsonBuilder.create(); try (var reader = Files.newBufferedReader(tempOutputFile); var jsonReader = gson.newJsonReader(reader)) { jsonReader.beginArray(); diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLintOutputDeserializer.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLintOutputDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..ad8039b066042a6297bade03664c67c7f617e6be --- /dev/null +++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/python/PyLintOutputDeserializer.java @@ -0,0 +1,50 @@ +package nl.tudelft.ewi.auta.checker.python; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import nl.tudelft.ewi.auta.common.model.PyLintResult; + +import java.lang.reflect.Type; + +/** + * A custom deserializer for a pylint results object. This is required to set the messageid field + * in the PyLintResult object, as Pylint stores this field as 'message-id' which cannot be a + * variable name in Java. + */ +public class PyLintOutputDeserializer implements JsonDeserializer<PyLintResult> { + /** + * Gson invokes this call-back method during deserialization when it encounters a field of the + * specified type. + * <p>In the implementation of this call-back method, you should consider invoking + * {@link JsonDeserializationContext#deserialize(JsonElement, Type)} method to create objects + * for any non-trivial field of the returned object. However, you should never invoke it on the + * the same type passing {@code json} since that will cause an infinite loop (Gson will call + * your + * call-back method again). + * + * @param json The Json data being deserialized + * @param typeOfT The type of the Object to deserialize to + * @param context the deserialization context. + * @return a deserialized object of the specified type typeOfT which is a subclass of {@code T} + * + * @throws com.google.gson.JsonParseException if json is not in the expected format + * of {@code typeofT} + */ + @Override + public PyLintResult deserialize(final JsonElement json, final Type typeOfT, + final JsonDeserializationContext context) { + final var jsonObject = json.getAsJsonObject(); + final var result = new PyLintResult(); + result.setType(jsonObject.get("type").getAsString()); + result.setModule(jsonObject.get("module").getAsString()); + result.setObj(jsonObject.get("obj").getAsString()); + result.setLine(jsonObject.get("line").getAsString()); + result.setColumn(jsonObject.get("column").getAsString()); + result.setPath(jsonObject.get("path").getAsString()); + result.setSymbol(jsonObject.get("symbol").getAsString()); + result.setMessage(jsonObject.get("message").getAsString()); + result.setMessageid(jsonObject.get("message-id").getAsString()); + return result; + } +} diff --git a/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java b/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java index d9ee7e02c3b88a7f5e195f4c4857790ac9a00421..a6656dcae1f228f1efd47b9095b691ae102033c2 100644 --- a/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java +++ b/worker/src/test/java/nl/tudelft/ewi/auta/checker/python/PyLintTest.java @@ -1,7 +1,6 @@ package nl.tudelft.ewi.auta.checker.python; import nl.tudelft.ewi.auta.Ob; -import nl.tudelft.ewi.auta.checker.java.TestSmellsAnalyzer; import nl.tudelft.ewi.auta.common.model.entity.Entity; import nl.tudelft.ewi.auta.common.model.entity.EntityLevel; import nl.tudelft.ewi.auta.common.model.entity.FileEntity; @@ -40,7 +39,7 @@ public class PyLintTest { final var python = new Python(new PythonDetector().findPython()); - try (var in = TestSmellsAnalyzer.class + try (var in = PyLintTest.class .getResourceAsStream("/lizard.zip"); var out = Files.newOutputStream(zipPath)) { IOUtils.copy(in, out);