diff --git a/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/application/ProblemJudgeService.java b/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/application/ProblemJudgeService.java index 75ca882..27b28f7 100644 --- a/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/application/ProblemJudgeService.java +++ b/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/application/ProblemJudgeService.java @@ -21,7 +21,7 @@ public class ProblemJudgeService { private final JudgeService judgeService; @Transactional(readOnly = true) - public ProblemJudgeResponse judgeProblem(String language, Long algorithmProblemId, String code) { + public synchronized ProblemJudgeResponse judgeProblem(String language, Long algorithmProblemId, String code) { CodeExecutor codeExecutor = judgeService.getCodeExecutor(language); diff --git a/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/AlgorithmProblem.java b/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/AlgorithmProblem.java index 75f3a38..b194054 100644 --- a/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/AlgorithmProblem.java +++ b/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/AlgorithmProblem.java @@ -1,7 +1,9 @@ package gooroommoon.algofi_compile.db.domain.entity; import jakarta.persistence.*; +import lombok.NoArgsConstructor; +@NoArgsConstructor @Entity @Table(name = "algorithmproblem") public class AlgorithmProblem { @@ -18,4 +20,11 @@ public class AlgorithmProblem { private Integer recommend_time; + public AlgorithmProblem(Long id, String title, String level, String content, Integer recommend_time) { + this.id = id; + this.title = title; + this.level = level; + this.content = content; + this.recommend_time = recommend_time; + } } diff --git a/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/TestCase.java b/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/TestCase.java index 8cddf3b..cd1451b 100644 --- a/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/TestCase.java +++ b/algofi-compile/src/main/java/gooroommoon/algofi_compile/db/domain/entity/TestCase.java @@ -2,7 +2,9 @@ import jakarta.persistence.*; import lombok.Getter; +import lombok.NoArgsConstructor; +@NoArgsConstructor @Entity @Table(name = "testcase") public class TestCase { @@ -20,4 +22,11 @@ public class TestCase { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "algorithmproblem_id") private AlgorithmProblem algorithmProblem; + + public TestCase(Long id, String testInput, String testOutput, AlgorithmProblem algorithmProblem) { + this.id = id; + this.testInput = testInput; + this.testOutput = testOutput; + this.algorithmProblem = algorithmProblem; + } } diff --git a/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeController.java b/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeController.java index 53e0704..e35fcac 100644 --- a/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeController.java +++ b/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeController.java @@ -6,32 +6,20 @@ import gooroommoon.algofi_compile.judge.exception.RequestException; import gooroommoon.algofi_compile.judge.exception.ServerException; import gooroommoon.algofi_compile.judge.service.JudgeResult; -import gooroommoon.algofi_compile.judge.service.JudgeService; -import gooroommoon.algofi_compile.judge.service.language.CodeExecutor; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.nio.file.Path; - @RestController @RequiredArgsConstructor @RequestMapping("/api/judge-input") public class InputJudgeController { - private final JudgeService judgeService; + private final InputJudgeService inputJudgeService; @PostMapping public ResponseEntity judgeInput(@RequestBody CodeExecutionRequest request) { - CodeExecutor codeExecutor = judgeService.getCodeExecutor(request.getLanguage()); - - Path path = codeExecutor.makeFileFromCode(request.getCode()); - - Process process = judgeService.executeCode(codeExecutor, path); - - StringBuilder output = insertInputAndGetOutput(request.getInput(), process, codeExecutor, path); - - String result = output.toString(); + String result = inputJudgeService.judgeInput(request.getLanguage(), request.getCode(), request.getInput()); CodeExecutionResponse response = generateResponse(request.getExpected(), result); return ResponseEntity.ok(response); } @@ -54,15 +42,6 @@ public ResponseEntity handleCodeExecutionException(CodeEx return new ResponseEntity<>(response, HttpStatus.OK); } - private StringBuilder insertInputAndGetOutput(String input, Process process, CodeExecutor codeExecutor, Path path) { - try { - return judgeService.insertInputAndGetOutput(process, input); - } finally { - judgeService.destroy(process); - codeExecutor.deleteFile(path); - } - } - private CodeExecutionResponse generateResponse(String expected, String result) { if (!isCorrect(expected, result)) { throw new CodeExecutionException(JudgeResult.WRONG_ANSWER); diff --git a/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeService.java b/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeService.java new file mode 100644 index 0000000..b5db338 --- /dev/null +++ b/algofi-compile/src/main/java/gooroommoon/algofi_compile/input/InputJudgeService.java @@ -0,0 +1,35 @@ +package gooroommoon.algofi_compile.input; + +import gooroommoon.algofi_compile.judge.service.JudgeService; +import gooroommoon.algofi_compile.judge.service.language.CodeExecutor; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.nio.file.Path; + +@Service +@RequiredArgsConstructor +public class InputJudgeService { + private final JudgeService judgeService; + + public synchronized String judgeInput(String language, String code, String input){ + CodeExecutor codeExecutor = judgeService.getCodeExecutor(language); + + Path path = codeExecutor.makeFileFromCode(code); + + Process process = judgeService.executeCode(codeExecutor, path); + + StringBuilder output = insertInputAndGetOutput(process, input, codeExecutor, path); + + return output.toString(); + } + + private StringBuilder insertInputAndGetOutput(Process process, String input, CodeExecutor codeExecutor, Path path) { + try { + return judgeService.insertInputAndGetOutput(process, input); + } finally { + judgeService.destroy(process); + codeExecutor.deleteFile(path); + } + } +} diff --git a/algofi-compile/src/main/resources/application-dev.properties b/algofi-compile/src/main/resources/application-dev.properties index 5507c32..66c1614 100644 --- a/algofi-compile/src/main/resources/application-dev.properties +++ b/algofi-compile/src/main/resources/application-dev.properties @@ -1,7 +1,2 @@ -spring.datasource.url=jdbc:h2:tcp://localhost/~/test -spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password= - spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=update \ No newline at end of file diff --git a/algofi-compile/src/test/java/gooroommoon/algofi_compile/db/presentation/ProblemJudgeControllerTest.java b/algofi-compile/src/test/java/gooroommoon/algofi_compile/db/presentation/ProblemJudgeControllerTest.java new file mode 100644 index 0000000..8eed6ac --- /dev/null +++ b/algofi-compile/src/test/java/gooroommoon/algofi_compile/db/presentation/ProblemJudgeControllerTest.java @@ -0,0 +1,163 @@ +package gooroommoon.algofi_compile.db.presentation; + +import gooroommoon.algofi_compile.db.application.ProblemJudgeService; +import gooroommoon.algofi_compile.db.domain.TestCaseRepository; +import gooroommoon.algofi_compile.db.domain.entity.AlgorithmProblem; +import gooroommoon.algofi_compile.db.domain.entity.TestCase; +import gooroommoon.algofi_compile.db.presentation.dto.ProblemJudgeRequest; +import gooroommoon.algofi_compile.db.presentation.dto.ProblemJudgeResponse; +import gooroommoon.algofi_compile.judge.exception.CodeExecutionException; +import gooroommoon.algofi_compile.judge.service.JudgeResult; +import gooroommoon.algofi_compile.judge.service.JudgeService; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.ResponseEntity; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@SpringBootTest +class ProblemJudgeControllerTest { + + @Mock + private TestCaseRepository testCaseRepository; + + @Test + void success() { + //given + AlgorithmProblem algorithmProblem = new AlgorithmProblem(1L, "문자 찾기","1","한 개의 문자열을 입력받고,특정 문자를 입력받아 해당 특정문자가 입력받은 문자열에 몇 개 존재하는지 알아내는 프로그램을 작성하세요. 대소문자를 구분하지 않습니다.문자열의 길이는 100을 넘지 않습니다.",10); + TestCase testCase = new TestCase(1L, "computerprogramming r","3", algorithmProblem); + when(testCaseRepository.findAllByAlgorithmProblemId(any())) + .thenReturn(List.of(testCase)); + + JudgeService judgeService = new JudgeService(); + + ProblemJudgeService problemJudgeService = new ProblemJudgeService(testCaseRepository, judgeService); + ProblemJudgeController problemJudgeController = new ProblemJudgeController(problemJudgeService); + + //when + ProblemJudgeRequest request = new ProblemJudgeRequest(1L, "java", + """ + import java.util.Scanner; + public class Main { + public int solution(String str, char c){ + int answer = 0; + str = str.toUpperCase(); + c = Character.toUpperCase(c); + for(char x : str.toCharArray()){ + if(x == c) answer++; + } + return answer; + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + Main T = new Main(); + String str = sc.next(); + char c = sc.next().charAt(0); + + System.out.println(T.solution(str, c)); + } + }"""); + ResponseEntity response = problemJudgeController.judgeProblem(request); + assertThat(Objects.requireNonNull(response.getBody()).getMessage()).isEqualTo(JudgeResult.ACCEPTED.getMessage()); + } + + @Test + void concurrencyTest() throws InterruptedException { + //given + AlgorithmProblem algorithmProblem = new AlgorithmProblem(1L, "문자 찾기","1","한 개의 문자열을 입력받고,특정 문자를 입력받아 해당 특정문자가 입력받은 문자열에 몇 개 존재하는지 알아내는 프로그램을 작성하세요. 대소문자를 구분하지 않습니다.문자열의 길이는 100을 넘지 않습니다.",10); + TestCase testCase = new TestCase(1L, "computerprogramming r","3", algorithmProblem); + when(testCaseRepository.findAllByAlgorithmProblemId(any())) + .thenReturn(List.of(testCase)); + + JudgeService judgeService = new JudgeService(); + + ProblemJudgeService problemJudgeService = new ProblemJudgeService(testCaseRepository, judgeService); + ProblemJudgeController problemJudgeController = new ProblemJudgeController(problemJudgeService); + + int threadNum = 2; + CountDownLatch doneSignal = new CountDownLatch(threadNum); + ExecutorService executorService = Executors.newFixedThreadPool(threadNum); + + ProblemJudgeRequest request1 = new ProblemJudgeRequest(1L, "java", + """ + import java.util.Scanner; + + public class Main { + public int solution(String str, char c){ + int answer = 0; + str = str.toUpperCase(); + c = Character.toUpperCase(c); + for(char x : str.toCharArray()){ + if(x == c) answer++; + } + return answer; + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + Main T = new Main(); + String str = sc.next(); + char c = sc.next().charAt(0); + + System.out.println(T.solution(str, c)); + } + }"""); + + ProblemJudgeRequest request2 = new ProblemJudgeRequest(1L, "java", + """ + + public class Main { + public static void main(String[] args) { + System.out.println(); + } + }"""); + + AtomicBoolean success1 = new AtomicBoolean(); + AtomicBoolean success2 = new AtomicBoolean(); + + //when + + executorService.execute(() -> { + try { + problemJudgeController.judgeProblem(request1); + success1.set(true); + } catch (CodeExecutionException e) { + success1.set(false); + } finally { + doneSignal.countDown(); + } + }); + + executorService.execute(() -> { + try { + problemJudgeController.judgeProblem(request2); + success2.set(true); + } catch (CodeExecutionException e) { + success2.set(false); + } finally { + doneSignal.countDown(); + } + }); + + doneSignal.await(); + executorService.shutdown(); + + //then + assertAll( + () -> assertThat(success1.get()).isTrue(), + () -> assertThat(success2.get()).isFalse() + ); + } +} \ No newline at end of file diff --git a/algofi-compile/src/test/java/gooroommoon/algofi_compile/input/InputJudgeControllerTest.java b/algofi-compile/src/test/java/gooroommoon/algofi_compile/input/InputJudgeControllerTest.java index 0a41e49..ce9ede6 100644 --- a/algofi-compile/src/test/java/gooroommoon/algofi_compile/input/InputJudgeControllerTest.java +++ b/algofi-compile/src/test/java/gooroommoon/algofi_compile/input/InputJudgeControllerTest.java @@ -15,9 +15,14 @@ import java.io.FileReader; import java.io.IOException; import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; @SpringBootTest class InputJudgeControllerTest { @@ -65,6 +70,17 @@ public void testJavaExecution() throws IOException { assertThat(Objects.requireNonNull(response.getBody()).getMessage()).isEqualTo(JudgeResult.ACCEPTED.getMessage()); } + @Test + public void javaFailure() throws IOException { + File file = new File("src/test/resources/fixture/HelloFailure.java"); + String javaCode = fileToString(file); + CodeExecutionRequest request = new CodeExecutionRequest(javaCode, "java", null, "Hello, Java\n"); + + assertThatThrownBy(() -> inputJudgeController.judgeInput(request)) + .isInstanceOf(CodeExecutionException.class) + .hasMessage(JudgeResult.WRONG_ANSWER.getMessage()); + } + @Test public void testPythonExecutionWithParam() throws IOException { File file = new File("src/test/resources/fixture/PrintInput.py"); @@ -151,6 +167,57 @@ public void withLeftBrace() throws IOException { assertThat(Objects.requireNonNull(response.getBody()).getMessage()).isEqualTo(JudgeResult.ACCEPTED.getMessage()); } + @Test + public void concurrencyTest() throws IOException, InterruptedException { + //given + int threadNum = 2; + CountDownLatch doneSignal = new CountDownLatch(threadNum); + ExecutorService executorService = Executors.newFixedThreadPool(threadNum); + + File file1 = new File("src/test/resources/fixture/Hello.java"); + String code1 = fileToString(file1); + CodeExecutionRequest request1 = new CodeExecutionRequest(code1, "java", "Hello, Java\n", "Hello, Java\n"); + + File file2 = new File("src/test/resources/fixture/HelloFailure.java"); + String code2 = fileToString(file2); + CodeExecutionRequest request2 = new CodeExecutionRequest(code2, "java", "Hello, Java\n", "Hello, Java\n"); + + AtomicBoolean success1 = new AtomicBoolean(); + AtomicBoolean success2 = new AtomicBoolean(); + + //when + executorService.execute(() -> { + try { + inputJudgeController.judgeInput(request1); + success1.set(true); + } catch (CodeExecutionException e) { + success1.set(false); + } finally { + doneSignal.countDown(); + } + }); + + executorService.execute(() -> { + try { + inputJudgeController.judgeInput(request2); + success2.set(true); + } catch (CodeExecutionException e) { + success2.set(false); + } finally { + doneSignal.countDown(); + } + }); + + doneSignal.await(); + executorService.shutdown(); + + //then + assertAll( + () -> assertThat(success1.get()).isTrue(), + () -> assertThat(success2.get()).isFalse() + ); + } + private String fileToString(File file) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new FileReader(file))) { diff --git a/algofi-compile/src/test/resources/fixture/HelloFailure.java b/algofi-compile/src/test/resources/fixture/HelloFailure.java new file mode 100644 index 0000000..b6863ab --- /dev/null +++ b/algofi-compile/src/test/resources/fixture/HelloFailure.java @@ -0,0 +1 @@ +public class Main { public static void main(String[] args) { System.out.println("Hello, Jav"); } } \ No newline at end of file