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");
}
}