From d1ae7807ac90e6c85022bd7812782ea65b0d6c36 Mon Sep 17 00:00:00 2001 From: GeunChang Ahn <13996827+rkaehdaos@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:29:08 +0900 Subject: [PATCH] Step4 carracing (#5541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * step1 squash * step2 squash * step3 squash * docs(README): STEP4 README.md 작성 * feat(car): Move, getDistance 구현 * feat(car): name 필드 * feat(car): Car 구현 완료 및 정리 * feat(Race): Race 생성자 및 getter 처리 * feat(Race): Race runRound - 이전 study test depreacated 주석처리 * feat(Input): Input 구현 * feat(result): 자동차 이름과 함께 움직임 출력 * feat(result): 최종 우승자 출력: 1명 이상 일 수 있음 * feat(result): result 구현 완료 * check: UI를 InputView, ResultView로 분리 * 메서드가 15라인을 넘어가지 않도록 구현 - pmd로 ncss 15line 강제화 - jdk21 apply - 기존 에 * 에러 정의 차이 리플렉션을 사용하여 생성자를 호출하면, 생성자 내부에서 발생하는 예외는 InvocationTargetException에 감싸져서 발생합니다. 이 때문에 예외가 직접적으로 UnsupportedOperationException가 아니라 InvocationTargetException으로 포착되는 것입니다. * 최적화 * Car 상수 사용 및 보완 * formatting * 가독성 * workflow 수정 * workflow 수정 * pmdtest4에 step4 넣음 * workflow에 pmd summary 추가 * 15라인 rule은 main 코드에만 ㅋㅋ * Extract PMD info and update step summary * test dependon pmdmain * pmd 실패여도 진행 * xmllint 사용 제거 * 메시지 추출 부분 수정 * 일단 원복 * awk로 개선된 스크립트 설명 파일 처리: awk를 사용하여 태그로 시작하고 태그로 끝나는 블록을 찾습니다. 이는 awk '//' 명령으로 처리합니다. 파일 내부 처리: awk 스크립트는 각 파일 블록을 처리하면서 파일 이름을 추출하고, 각 위반 사항의 정보를 파싱합니다. 파일 이름은 gensub() 함수를 사용하여 추출하고, 위반의 라인 번호, 메시지, URL을 추출하여 출력합니다. Markdown 형식: 추출된 정보는 콘솔에 출력되고 pmd_summary.md 파일에 Markdown 형식으로 기록됩니다. 최종적으로, 이 Markdown 파일은 GitHub Actions 단계 요약에 추가됩니다. 장점 성능: awk는 스트림을 처리하면서 발생하는 데이터를 효율적으로 처리할 수 있으므로, 큰 XML 파일에 대해서도 성능상의 이점이 있습니다. 간결성: awk를 사용하면 복잡한 grep 및 sed 조합을 하나의 명령으로 대체할 수 있어 스크립트가 더욱 간결해집니다. 유지 관리: 스크립트가 간결해지므로 유지 관리가 용이해지며, 변경 사항을 적용하기 쉬워집니다. * awk로 개선된 스크립트2 - 텍스트 한정 변경된 부분 설명 태그 내용 처리 로직: 태그를 만나면 내용 캡처를 시작하고, 태그를 만나면 내용 캡처를 종료하며, 그 사이에 있는 모든 텍스트를 content 변수에 누적합니다. 텍스트 출력: 최종적으로 각 violation의 설명이 content 변수에 저장되고, 이를 Markdown 링크의 텍스트로 사용하여 출력합니다. 이 수정을 통해 PMD 요약 정보에는 각 violation의 상세 설명이 태그 사이의 텍스트로 정확히 제공됩니다. 이 방법은 XML 데이터에서 필요한 정보만을 추출하여 보다 명확하고 유용한 요약을 생성하는 데 도움이 됩니다. * message 수정 * workflow-PR 생성 * NCSS 5->15 * NCSS 5->15 * NCSS 15->5 * README.md 수정 * onPRTest * onPRTest -             $GITHUB_STEP_SUMMARY * onPRTest * Comment SUMMARY Report on PR body check * body-path: 'PR_summary.md' * body-path: 'PR_summary.md' * PUSH_summary.md 에 모으기 * PR_summary.md 에 total count 추가 * push workflow는 pr이 있으면 작동하지 않음 * pr workflow type 지정 * PR시에는 build task * github.event.pull_request삭제 - pr workflow에서만 true 가능 * types 가독성 * graalvm 적용 * 정리 * 언어 정리 * pmd pmdtest는 전부 제외, pmdmin은 step4만 * 메서드라인은 15라인 * 문제가 없을시 pmd report 요약 액션 에러 수정 * push workflow - 문제가 없을시 pmd report 요약 액션 에러 수정 * PR 멈추기 * push test * push test2 * error 수정: branches-ignore: ['*'] * total_violations 출력 수정 * 극단적 test * 극단적 test2 * 극단적 test3 * PMD 리포트 요약 복원 * grep 명령 실패 허용 * jacoco summary 지워지는 거 방지 * PR workflow 활성화 --------- Co-authored-by: GeunChang Ahn <13996827+rkaehdaos@users.noreply.github.com> * Step4 - 단일 책임원칙 확인 * psvm 클래스 rename * 패키지 이동 * 메서드명 변경 * CarRacingRunner 생성 * 단일책임 원칙 --------- Co-authored-by: GeunChang Ahn <13996827+rkaehdaos@users.noreply.github.com> * Merge remote-tracking branch 'origin/step4_carracing' into step4_carr… (#3) conflict 해결 ㅠ * conflict 해결 및 전략 패턴 적용 (#4) * conflict ㅎㅐ결 * RandomMovingStrategy 작성 * stragegy 패턴 적용 * 포맷팅 --- .github/pmd/ruleset.xml | 18 +++ .github/workflows/PR-mytest.yml | 113 ++++++++++++++++++ .github/workflows/push-test.yml | 73 ++++++++--- README.md | 52 ++++---- build.gradle | 39 +++++- gradle/wrapper/gradle-wrapper.properties | 4 +- .../java/step4_winner/CarRacingRunner.java | 49 ++++++++ .../java/step4_winner/Step4Application.java | 22 ++++ src/main/java/step4_winner/domain/Car.java | 23 ++++ src/main/java/step4_winner/domain/Race.java | 22 ++++ .../step4_winner/strategy/MovingStrategy.java | 5 + .../strategy/RandomMovingStrategy.java | 15 +++ .../java/step4_winner/view/InputView.java | 21 ++++ .../java/step4_winner/view/ResultView.java | 32 +++++ src/main/resources/pmd_ruleset_2_0_0.xsd | 65 ++++++++++ .../step3_CarRacing/CarRacingGameTest.java | 2 +- .../step4_winner/Step4ApplicationTest.java | 93 ++++++++++++++ .../java/step4_winner/domain/CarTest.java | 50 ++++++++ .../java/step4_winner/domain/RaceTest.java | 57 +++++++++ src/test/java/study/AssertJTest.java | 2 +- src/test/java/study/StringTest.java | 4 +- 21 files changed, 715 insertions(+), 46 deletions(-) create mode 100644 .github/pmd/ruleset.xml create mode 100644 .github/workflows/PR-mytest.yml create mode 100644 src/main/java/step4_winner/CarRacingRunner.java create mode 100644 src/main/java/step4_winner/Step4Application.java create mode 100644 src/main/java/step4_winner/domain/Car.java create mode 100644 src/main/java/step4_winner/domain/Race.java create mode 100644 src/main/java/step4_winner/strategy/MovingStrategy.java create mode 100644 src/main/java/step4_winner/strategy/RandomMovingStrategy.java create mode 100644 src/main/java/step4_winner/view/InputView.java create mode 100644 src/main/java/step4_winner/view/ResultView.java create mode 100644 src/main/resources/pmd_ruleset_2_0_0.xsd create mode 100644 src/test/java/step4_winner/Step4ApplicationTest.java create mode 100644 src/test/java/step4_winner/domain/CarTest.java create mode 100644 src/test/java/step4_winner/domain/RaceTest.java diff --git a/.github/pmd/ruleset.xml b/.github/pmd/ruleset.xml new file mode 100644 index 00000000000..4028cf97e8d --- /dev/null +++ b/.github/pmd/ruleset.xml @@ -0,0 +1,18 @@ + + + + 메서드 길이를 15줄로 제한하는 custom ruleset + + + + + + + + + + + diff --git a/.github/workflows/PR-mytest.yml b/.github/workflows/PR-mytest.yml new file mode 100644 index 00000000000..e1fc2e9ea70 --- /dev/null +++ b/.github/workflows/PR-mytest.yml @@ -0,0 +1,113 @@ +name: Gradle PR test On GitHub Action +on: + pull_request: + types: [opened,reopened,synchronize] + +jobs: + onPRTest: + runs-on: ubuntu-latest + steps: + - name: '소스 checkout' + uses: actions/checkout@master + + - name: 'graalvm jdk21 setup' + uses: graalvm/setup-graalvm@v1 + with: + java-version: '21' + distribution: 'graalvm' + github-token: ${{ secrets.GITHUB_TOKEN }} + native-image-job-reports: 'true' + cache: 'gradle' + + - name: 'gradle 빌드' + run: gradle build --no-daemon --parallel + + - name: '커버리지 정보 및 요약' + run: | + echo 'CSV 파일에서 커버리지 정보 추출'; + awk -F',' 'NR > 1 {instructions_covered += $5; instructions_missed += $4; branches_covered += $7; branches_missed += $6} END {print instructions_covered, instructions_missed, branches_covered, branches_missed}' build/reports/jacoco/test/jacocoTestReport.csv > coverage.txt + read instructions_covered instructions_missed branches_covered branches_missed < coverage.txt + echo '커버리지 계산'; + total_instructions=$((instructions_covered + instructions_missed)) + total_branches=$((branches_covered + branches_missed)) + instruction_coverage=$(echo "scale=2; $instructions_covered / $total_instructions * 100" | bc) + echo '분모가 0일 경우, 커버리지를 'N/A'로 설정'; + if [ "$total_instructions" -eq 0 ]; then + instruction_coverage="N/A" + else + instruction_coverage=$(echo "scale=2; $instructions_covered / $total_instructions * 100" | bc)% + fi + if [ "$total_branches" -eq 0 ]; then + branch_coverage="N/A" + else + branch_coverage=$(echo "scale=2; $branches_covered / $total_branches * 100" | bc)% + fi + echo '# GitHub Action Summary' >> PR_summary.md + echo 'GITHUB_STEP_SUMMARY에 커버리지 정보 추가'; + echo "## JaCoCo 커버리지 요약" >> PR_summary.md + echo "- Instruction Coverage: $instruction_coverage" >> PR_summary.md + echo "- Branch Coverage: $branch_coverage" >> PR_summary.md + + - name: 'PMD 리포트 요약' + run: | + echo 'PMD 파일에서 정보 추출: XML 구조에 의존적이므로 구조가 변경되면 스크립트도 업데이트가 필요'; + echo 'xml이 매우 크면 성능에 문제가 생길 수 있으므로 더 효율적인 파싱 방법의 고려가 필요'; + echo "## PMD Code Analysis" >> PR_summary.md + total_violations=$(grep -c '> PR_summary.md + else + echo "### Total Violations: $total_violations" >> PR_summary.md + echo '각 file 태그를 찾아 파일 경로를 추출하고, 내부의 violation 정보를 처리' + awk '//' build/reports/pmd/main.xml | awk ' + /> "PR_summary.md"; + next; + } + // { + if ($0 ~ //) { + print "- Line " line ": [" content "](" url ")"; + print "- Line " line ": [" content "](" url ")" >> "PR_summary.md"; + capturing = 0; + } else if (capturing) { + content = content $0; + } + } + ' + fi + cat PR_summary.md >> $GITHUB_STEP_SUMMARY + + - name: 'PR Comment에 SUMMARY Report 작성' + if: github.repository == 'rkaehdaos/java-racingcar' + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body-path: 'PR_summary.md' + + - name: '실패 시 보고서 업로드' + uses: actions/upload-artifact@v4 + if: failure() + with: + name: report-jacoco + path: | + build/reports/jacoco/test/html + build/reports/pmd/*.html + + - name: 'slack 알림' + uses: 8398a7/action-slack@v3 + if: github.repository == 'rkaehdaos/java-racingcar' + with: + status: ${{ job.status }} + author_name: my workflow bot + fields: repo,message,commit,author,eventName,ref,workflow,job,took, + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/push-test.yml b/.github/workflows/push-test.yml index c5874acee29..08fc3289b84 100644 --- a/.github/workflows/push-test.yml +++ b/.github/workflows/push-test.yml @@ -1,29 +1,28 @@ name: Gradle test On GitHub Action on: push: - branches-ignore: [ 'main','master','rkaehdaos' ] + branches-ignore: ['main','master'] jobs: onPushTest: - if: github.event.pull_request.opened == false runs-on: ubuntu-latest steps: - - name: Checkout source code + - name: '소스 checkout' uses: actions/checkout@master - - name: setup-java-21 + - name: 'jdk21 setup' uses: actions/setup-java@v4 with: - distribution: 'zulu' # See 'Supported distributions' for available options - java-version: '11' + distribution: 'zulu' + java-version: '21' cache: 'gradle' - - name: test + - name: 'gradle test' run: | echo "Available processors: $(nproc)" - gradle test jacocoTestCoverageVerification --no-daemon --parallel + gradle test --no-daemon --parallel - - name: Extract coverage info and update step summary + - name: '커버리지 정보 및 요약' run: | echo 'CSV 파일에서 커버리지 정보 추출'; awk -F',' 'NR > 1 {instructions_covered += $5; instructions_missed += $4; branches_covered += $7; branches_missed += $6} END {print instructions_covered, instructions_missed, branches_covered, branches_missed}' build/reports/jacoco/test/jacocoTestReport.csv > coverage.txt @@ -43,25 +42,63 @@ jobs: else branch_coverage=$(echo "scale=2; $branches_covered / $total_branches * 100" | bc)% fi + echo '# GitHub Action Summary' >> PUSH_summary.md echo 'GITHUB_STEP_SUMMARY에 커버리지 정보 추가'; - echo "JaCoCo 커버리지 요약" >> $GITHUB_STEP_SUMMARY - echo "Instruction Coverage: $instruction_coverage" >> $GITHUB_STEP_SUMMARY - echo "Branch Coverage: $branch_coverage" >> $GITHUB_STEP_SUMMARY + echo "## JaCoCo 커버리지 요약" >> PUSH_summary.md + echo "- Instruction Coverage: $instruction_coverage" >> PUSH_summary.md + echo "- Branch Coverage: $branch_coverage" >> PUSH_summary.md - - name: Upload JaCoCo coverage report + - name: 'PMD 리포트 요약' + run: | + echo 'PMD 파일에서 정보 추출: XML 구조에 의존적이므로 구조가 변경되면 스크립트도 업데이트가 필요'; + echo 'xml이 매우 크면 성능에 문제가 생길 수 있으므로 더 효율적인 파싱 방법의 고려가 필요'; + echo "## PMD Code Analysis" >> PUSH_summary.md + total_violations=$(grep -c '> PUSH_summary.md + else + echo "### Total Violations: $total_violations" >> PUSH_summary.md + echo '각 file 태그를 찾아 파일 경로를 추출하고, 내부의 violation 정보를 처리' + awk '//' build/reports/pmd/main.xml | awk ' + /> "PUSH_summary.md"; + next; + } + // { + if ($0 ~ //) { + print "- Line " line ": [" content "](" url ")"; + print "- Line " line ": [" content "](" url ")" >> "PUSH_summary.md"; + capturing = 0; + } else if (capturing) { + content = content $0; + } + } + ' + fi + cat PUSH_summary.md >> $GITHUB_STEP_SUMMARY + + - name: '실패 시 보고서 업로드' uses: actions/upload-artifact@v4 if: failure() with: - name: jacoco-report - path: build/reports/jacoco/test/html + name: report-jacoco + path: | + build/reports/jacoco/test/html + build/reports/pmd/*.html - - name: Notification + - name: 'slack 알림' uses: 8398a7/action-slack@v3 if: always() with: status: ${{ job.status }} - author_name: GeunChang Ahn - job_name: onPushTest + author_name: my workflow bot fields: repo,message,commit,author,eventName,ref,workflow,job,took, env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 0937b642afd..3f0290db0ae 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,36 @@ -# 자동차 경주 게임 -## 진행 방법 -* 자동차 경주 게임 요구사항을 파악한다. -* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. -* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. -* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. +# 🚀 4단계 - 자동차 경주(우승자) -## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) +- 구현할 기능 목록 단위 추가 +- commit 단위는 정리한 기능 목록 단위로 추가할 것!! -UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. +## 기능 요구사항 +- [x] 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가 +- [x] Car 구현 + - [x] move 구현: random4이상시 움직임 + - [x] getDistance: 차의 움직인 거리 getter + - [x] 이름: 5자 제한, 생성할때 부여, getter 필요, Lombok `@getter` 처리 +- [] Race 구현 + - [x] Race 생성자 : 자동차들 list 준비, + - [x] 자동차 리스트 가져오기: `@getter` 처리 + - [x] Race runRound +- [x] Input 구현 + - [x] 자동차 이름 입력 + - [x] : validate: 5자 : 이미 Car 생성자에서 구현 -## 기능 목록 및 commit 로그 요구사항 -- commit message 종류 구분 -```text -feat (feature) -fix (bug fix) -docs (documentation) -style (formatting, missing semi colons, …) -refactor -test (when adding missing tests) -chore (maintain) -``` \ No newline at end of file +- [x] result 구현 + - [x] 자동차 이름과 함께 움직임 출력 + - [x] 최종 우승자 출력: 1명 이상 일 수 있음 + +## Check 사항 +- [ ] 들여쓰기 1까지인지 확인 +- [x] 메서드가 15라인을 넘어가지 않도록 구현 + - [x] ~~pmd 15 line 적용 시도~~ + - https://github.com/pmd/pmd/issues/2127#issue-527718378 + - Enforcing length limits with LoC("lines of code") is not very meaningful, could even be called a bad practice + - **LoC로 길이 제한을 적용하는 것은 그다지 의미가 없으며, 심지어 나쁜 관행이라고 할 수도 있습니다** + - [x] NCSS 15 라인 설정 - PMD Custom Rule apply + +- [x] 단일 책임 원칙으로 메서드가 되어 있는지 확인 +- [ ] 모든 로직에 단위 테스트 구현 - UI 제외 +- [x] UI를 `InputView`, `ResultView`로 분리 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1c3e4177b75..410c1536696 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'java' id 'jacoco' + id 'pmd' } version '1.0' @@ -19,14 +20,45 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' } +// java java { toolchain { - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(21) } } +tasks.named('compileJava') { + dependsOn clean +} + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" +} + +// pmd +pmd { + toolVersion = "7.0.0" + consoleOutput = true + ignoreFailures = true + incrementalAnalysis = true + ruleSets = [] + ruleSetFiles = files(rootProject.file(".github/pmd/ruleset.xml") as Object) +} + +tasks.named('pmdMain') { + // excludes =['**/*',] + includes = ['**/step4_winner/*',] +// ruleSetFiles = files(rootProject.file(".github/pmd/ruleset.xml") as Object) +} + +tasks.named('pmdTest') { + excludes =['**/*',] +// includes = ['**/step4_winner/*',] +} + tasks.named('test') { - useJUnitPlatform() // Junit5 사용: 테스트 종속성에 JUnit Jupiter API와 JUnit Jupiter Engine을 포함해야 함. + dependsOn pmdMain + useJUnitPlatform() maxParallelForks = Runtime.runtime.availableProcessors() finalizedBy jacocoTestCoverageVerification } @@ -35,4 +67,7 @@ jacocoTestReport { } jacocoTestCoverageVerification { dependsOn jacocoTestReport + violationRules { + rule { limit { minimum = 0.8 } } + } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 28ff446a215..a8382d7c42b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/src/main/java/step4_winner/CarRacingRunner.java b/src/main/java/step4_winner/CarRacingRunner.java new file mode 100644 index 00000000000..41625226c55 --- /dev/null +++ b/src/main/java/step4_winner/CarRacingRunner.java @@ -0,0 +1,49 @@ +package step4_winner; + +import step4_winner.domain.Car; +import step4_winner.domain.Race; +import step4_winner.strategy.MovingStrategy; +import step4_winner.strategy.RandomMovingStrategy; +import step4_winner.view.InputView; +import step4_winner.view.ResultView; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class CarRacingRunner { + private final InputView inputView; + private final ResultView resultView; + private final MovingStrategy movingStrategy; + private final Race race; + private final int tries; + + public CarRacingRunner(InputView inputView, ResultView resultView) { + this.inputView = inputView; + this.resultView = resultView; + this.race = prepareRace(); + this.tries = prepareTries(); + this.movingStrategy = new RandomMovingStrategy(); + } + + private Race prepareRace() { + String carNamesWithComma = inputView.getCarNamesWithComma(); + List cars = Arrays.stream(carNamesWithComma.split(",")) + .map(Car::new) + .collect(Collectors.toList()); + return new Race(cars); + } + + private int prepareTries() { + return inputView.getNumberOfRounds(); + } + + public void runRaces() { + resultView.printResultHeader(); + for (int i = 0; i < tries; i++) { + race.runRace(movingStrategy); + resultView.displayRaceResult(race.getCars()); + } + resultView.printFinalWinner(race.getCars()); + } +} diff --git a/src/main/java/step4_winner/Step4Application.java b/src/main/java/step4_winner/Step4Application.java new file mode 100644 index 00000000000..0180ebdd62b --- /dev/null +++ b/src/main/java/step4_winner/Step4Application.java @@ -0,0 +1,22 @@ +package step4_winner; + +import step4_winner.view.InputView; +import step4_winner.view.ResultView; + +import java.util.Scanner; + +public final class Step4Application { + private Step4Application() { + throw new UnsupportedOperationException("이 유틸 클래스는 인스턴스화할 수 없습니다."); + } + + public static void main(String[] args) { + // init + InputView inputView = new InputView(new Scanner(System.in)); + ResultView resultView = new ResultView(); + CarRacingRunner carRacingRunner = new CarRacingRunner(inputView, resultView); + + // run races + carRacingRunner.runRaces(); + } +} diff --git a/src/main/java/step4_winner/domain/Car.java b/src/main/java/step4_winner/domain/Car.java new file mode 100644 index 00000000000..bdb2ba3a764 --- /dev/null +++ b/src/main/java/step4_winner/domain/Car.java @@ -0,0 +1,23 @@ +package step4_winner.domain; + +import lombok.Getter; +import step4_winner.strategy.MovingStrategy; + +@Getter +public class Car { + private static final int MAX_NAME_LENGTH = 5; + private static final int MIN_DISTANCE = 1; + + private int distance = MIN_DISTANCE; + private final String name; + + public Car(String inputName) { + if (inputName.length() > MAX_NAME_LENGTH) + throw new IllegalArgumentException("5자를 초과할 수 없습니다"); + this.name = inputName; + } + + public void move(MovingStrategy movingStrategy) { + if (movingStrategy.isMove()) distance++; + } +} diff --git a/src/main/java/step4_winner/domain/Race.java b/src/main/java/step4_winner/domain/Race.java new file mode 100644 index 00000000000..20294a8aad0 --- /dev/null +++ b/src/main/java/step4_winner/domain/Race.java @@ -0,0 +1,22 @@ +package step4_winner.domain; + + +import lombok.Getter; +import step4_winner.strategy.MovingStrategy; + +import java.util.List; + +@Getter +public class Race { + private final List cars; + + public Race(List cars) { + if (cars.isEmpty()) + throw new IllegalArgumentException("0이상이어야 함"); + this.cars = cars; + } + + public void runRace(MovingStrategy movingStrategy) { + cars.forEach(car -> car.move(movingStrategy)); + } +} diff --git a/src/main/java/step4_winner/strategy/MovingStrategy.java b/src/main/java/step4_winner/strategy/MovingStrategy.java new file mode 100644 index 00000000000..97575e5ba65 --- /dev/null +++ b/src/main/java/step4_winner/strategy/MovingStrategy.java @@ -0,0 +1,5 @@ +package step4_winner.strategy; + +public interface MovingStrategy { + boolean isMove(); +} diff --git a/src/main/java/step4_winner/strategy/RandomMovingStrategy.java b/src/main/java/step4_winner/strategy/RandomMovingStrategy.java new file mode 100644 index 00000000000..2f55b38e87d --- /dev/null +++ b/src/main/java/step4_winner/strategy/RandomMovingStrategy.java @@ -0,0 +1,15 @@ +package step4_winner.strategy; + +import java.util.Random; + +public class RandomMovingStrategy implements MovingStrategy { + + private final Random random = new Random(); + private static final int RANDOM_RANGE = 10; + private static final int MOVE_THRESHOLD = 4; + + @Override + public boolean isMove() { + return random.nextInt(RANDOM_RANGE) > MOVE_THRESHOLD; + } +} diff --git a/src/main/java/step4_winner/view/InputView.java b/src/main/java/step4_winner/view/InputView.java new file mode 100644 index 00000000000..bdb8fa797e8 --- /dev/null +++ b/src/main/java/step4_winner/view/InputView.java @@ -0,0 +1,21 @@ +package step4_winner.view; + +import java.util.Scanner; + +public class InputView { + private final Scanner scanner; + + public InputView(Scanner scanner) { + this.scanner = scanner; + } + + public String getCarNamesWithComma() { + System.out.println("경주할 자동차 이름들을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + return scanner.next(); + } + + public int getNumberOfRounds() { + System.out.println("시도할 회수는 몇 회 인가요?"); + return scanner.nextInt(); + } +} diff --git a/src/main/java/step4_winner/view/ResultView.java b/src/main/java/step4_winner/view/ResultView.java new file mode 100644 index 00000000000..b615c64b5e5 --- /dev/null +++ b/src/main/java/step4_winner/view/ResultView.java @@ -0,0 +1,32 @@ +package step4_winner.view; + +import step4_winner.domain.Car; + +import java.util.List; +import java.util.stream.Collectors; + +public class ResultView { + + public void printResultHeader() { + System.out.print("실행 결과\n"); + } + + public void displayRaceResult(List cars) { + cars.forEach(car -> System.out.println(car.getName() + " : " + "-".repeat(car.getDistance()))); + System.out.println(); + } + + public void printFinalWinner(List cars) { + int maxDistance = cars.stream() + .mapToInt(Car::getDistance) + .max() + .orElseThrow(() -> new IllegalStateException("List is empty ")); + + String winners = cars.stream() + .filter(car -> car.getDistance() == maxDistance) + .map(Car::getName) + .collect(Collectors.joining(", ")); + + System.out.println(winners + "가 최종 우승했습니다."); + } +} diff --git a/src/main/resources/pmd_ruleset_2_0_0.xsd b/src/main/resources/pmd_ruleset_2_0_0.xsd new file mode 100644 index 00000000000..b42d7e0bdee --- /dev/null +++ b/src/main/resources/pmd_ruleset_2_0_0.xsd @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/step3_CarRacing/CarRacingGameTest.java b/src/test/java/step3_CarRacing/CarRacingGameTest.java index 0871a8078ea..89ab111fbc3 100644 --- a/src/test/java/step3_CarRacing/CarRacingGameTest.java +++ b/src/test/java/step3_CarRacing/CarRacingGameTest.java @@ -76,7 +76,7 @@ void CarMoveTest() { assertThat(car.getDistance()).isEqualTo(2); } - @RepeatedTest(10000) + @Test @DisplayName("100개의 차가 참가한 race가 일단 열리면, 100대중 1개는 전진 한다") public void RaceRoundTest() { Race race = new Race(100); diff --git a/src/test/java/step4_winner/Step4ApplicationTest.java b/src/test/java/step4_winner/Step4ApplicationTest.java new file mode 100644 index 00000000000..4ae7605569c --- /dev/null +++ b/src/test/java/step4_winner/Step4ApplicationTest.java @@ -0,0 +1,93 @@ +package step4_winner; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.NoSuchElementException; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class Step4ApplicationTest { + + private final InputStream stdIn = System.in; + private final PrintStream stdOut = System.out; + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + + @BeforeEach + void setUpPrintStream() { + System.setOut(new PrintStream(outContent)); + } + + @AfterEach + void restoreSystemIn() { + System.setIn(stdIn); + System.setOut(stdOut); + } + + @Test + @DisplayName("CarRacingWinner는 인스턴스화 할 수 없다") + void NonInstanceCarRacingWinner() throws NoSuchMethodException { + // GIVEN + Constructor constructor = Step4Application.class.getDeclaredConstructor(); + assertThat(constructor.canAccess(null)).isFalse(); + + // WHEN + constructor.setAccessible(true); + Throwable throwable = catchThrowable(constructor::newInstance); + + // 리플렉션을 사용하여 생성자를 호출하면, 생성자 내부에서 발생하는 예외는 InvocationTargetException에 감싸져서 발생 + assertThat(throwable).isInstanceOf(InvocationTargetException.class); + // 그 내부의 cause를 검사해서 실제 UnsupportedOperationException가 발생하는지 확인 + assertThat(throwable.getCause()).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + @DisplayName("Input Test") + void CarRacingWinnerInputTest() { + // GIVEN 1번만 시도 + provideInput("a,b,c,d,e,f,g", "1"); + + // WHEN + Step4Application.main(new String[]{}); + + // THEN + String outputStreamCaptorString = outContent.toString(); + String raceResult = outputStreamCaptorString.split("\n실행 결과\n", 2)[1]; + + long linesCount = raceResult.lines().count(); + assertThat(linesCount).isEqualTo(9L); + + long emptyLinesCount = raceResult.lines().filter(String::isEmpty).count(); + assertThat(emptyLinesCount).isEqualTo(1L); + + } + + @Test + @DisplayName("입력된 값이 없으면 NoSuchElementException") + void throwNoSuchElementExceptionIfNoInput() { + // GIVEN + provideInput("", ""); + + // WHEN + Throwable IllegalThrown = catchThrowable(() -> Step4Application.main(new String[]{})); + + // THEN + assertThat(IllegalThrown) + .isInstanceOf(NoSuchElementException.class); + } + + private void provideInput(String... strings) { + String inputStr = String.join("\n", strings); + System.setIn(new ByteArrayInputStream(inputStr.getBytes(UTF_8))); + } +} \ No newline at end of file diff --git a/src/test/java/step4_winner/domain/CarTest.java b/src/test/java/step4_winner/domain/CarTest.java new file mode 100644 index 00000000000..2aeace113bb --- /dev/null +++ b/src/test/java/step4_winner/domain/CarTest.java @@ -0,0 +1,50 @@ +package step4_winner.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +class CarTest { + + @Test + @DisplayName("자동차의 기본 거리는 1이고, move에 4이상의 값이 주어지면 distance가 증가한다") + void CarMoveTest() { + + // GIVEN + String legalName = "test1"; + + // WHEN + Car car = new Car(legalName); //이름 넣도록 추가 + + // THEN + assertThat(car.getDistance()).isEqualTo(1); + assertThat(car.getName()).isEqualTo("test1"); + + // WHEN : 안움직임 + car.move(() -> false); + // THEN + assertThat(car.getDistance()).isEqualTo(1); + + // WHEN : 움직임 + car.move(() -> true); + // THEN + assertThat(car.getDistance()).isGreaterThan(1); + } + + @Test + @DisplayName("자동차 이름은 5자를 초과할 수 없다") + void carNameNotLongerThan5Test() { + // GIVEN + String IllegalCarName = "5charover"; + + // WHEN + Throwable IllegalThrown = catchThrowable(() -> new Car(IllegalCarName)); + + // THEN + assertThat(IllegalThrown) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("5자를 초과할 수 없습니다"); + } +} \ No newline at end of file diff --git a/src/test/java/step4_winner/domain/RaceTest.java b/src/test/java/step4_winner/domain/RaceTest.java new file mode 100644 index 00000000000..0505b693388 --- /dev/null +++ b/src/test/java/step4_winner/domain/RaceTest.java @@ -0,0 +1,57 @@ +package step4_winner.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import step4_winner.strategy.RandomMovingStrategy; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + + +class RaceTest { + + @Test + @DisplayName("레이스에는 1대 이상의 차가 필요하다.") + void raceMustPositiveCars() { + // GIVEN empty list + List cars = new ArrayList<>(); + // WHEN create + Throwable illegalThrown = catchThrowable(() -> new Race(cars)); + // THEN + assertThat(illegalThrown) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("0이상이어야 함"); + + // GIVEN positive + cars.add(new Car("test1")); + // WHEN + Race legalRace = new Race(cars); + // THEN + assertThat(legalRace).isNotNull(); + } + + @Test + @DisplayName("100개의 차가 참가한 race가 일단 열리면, 100대중 1개는 전진 한다") + public void RaceRoundTest() { + // GIVEN + List cars = IntStream.range(0, 100) + .mapToObj(i -> "c_" + (++i)) + .peek(System.out::println) + .map(Car::new) + .collect(Collectors.toList()); + Race race = new Race(cars); + + // WHEN + race.runRace(new RandomMovingStrategy()); + + // then + boolean anyCarMoved = cars.stream().anyMatch(car -> car.getDistance() > 1); + assertThat(anyCarMoved).isTrue(); + } + +} \ No newline at end of file diff --git a/src/test/java/study/AssertJTest.java b/src/test/java/study/AssertJTest.java index b92ce5cc776..a0271ff1bc0 100644 --- a/src/test/java/study/AssertJTest.java +++ b/src/test/java/study/AssertJTest.java @@ -38,7 +38,7 @@ protected Dog(String name, Float weight) { // 재귀적 필드 비교 -> 성공 // deprecated - assertThat(fido).isEqualToComparingFieldByFieldRecursively(fidosClone); +// assertThat(fido).isEqualToComparingFieldByFieldRecursively(fidosClone); // 위를 대체할 수 있는 새로운 메서드 // 더 많은 유연성과 더 나은 REPORT, 더 쉬운 사용법 diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java index 209a33c0846..11972f77dfc 100644 --- a/src/test/java/study/StringTest.java +++ b/src/test/java/study/StringTest.java @@ -29,11 +29,11 @@ void studyStringcharAt() { "(1,2)".charAt(20); }) .isInstanceOf(IndexOutOfBoundsException.class) - .hasMessageContaining("String index out of range:"); + .hasMessageContaining("out of bounds for length"); assertThatThrownBy(() -> { "(1,2)".charAt(-10); }) .isInstanceOf(IndexOutOfBoundsException.class) - .hasMessageContaining("String index out of range:"); + .hasMessageContaining("out of bounds for length"); } }