diff --git a/backend/src/main/java/heartbeat/config/WebConfig.java b/backend/src/main/java/heartbeat/config/WebConfig.java index 36f42ef305..91445b5f5e 100644 --- a/backend/src/main/java/heartbeat/config/WebConfig.java +++ b/backend/src/main/java/heartbeat/config/WebConfig.java @@ -2,6 +2,7 @@ import heartbeat.controller.board.dto.request.BoardType; import heartbeat.controller.report.dto.request.DataType; +import heartbeat.controller.report.dto.request.ReportType; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.format.FormatterRegistry; @@ -25,6 +26,13 @@ public DataType convert(String source) { return DataType.fromValue(source); } }); + + registry.addConverter(new Converter() { + @Override + public ReportType convert(String type) { + return ReportType.fromValue(type); + } + }); } } diff --git a/backend/src/main/java/heartbeat/controller/report/GenerateReportController.java b/backend/src/main/java/heartbeat/controller/report/GenerateReportController.java index 7dd11eca1e..83687ddd01 100644 --- a/backend/src/main/java/heartbeat/controller/report/GenerateReportController.java +++ b/backend/src/main/java/heartbeat/controller/report/GenerateReportController.java @@ -1,15 +1,12 @@ package heartbeat.controller.report; import heartbeat.controller.report.dto.request.DataType; +import heartbeat.controller.report.dto.request.ReportType; +import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.ExportCSVRequest; -import heartbeat.controller.report.dto.request.GenerateBoardReportRequest; -import heartbeat.controller.report.dto.request.GenerateDoraReportRequest; import heartbeat.controller.report.dto.response.CallbackResponse; import heartbeat.controller.report.dto.response.ReportResponse; -import heartbeat.exception.BaseException; -import heartbeat.handler.AsyncExceptionHandler; import heartbeat.service.report.GenerateReporterService; -import heartbeat.util.IdUtil; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; @@ -17,12 +14,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PostMapping; import java.util.concurrent.CompletableFuture; @@ -35,8 +32,6 @@ public class GenerateReportController { private final GenerateReporterService generateReporterService; - private final AsyncExceptionHandler asyncExceptionHandler; - @Value("${callback.interval}") private Integer interval; @@ -62,62 +57,15 @@ public ResponseEntity generateReport(@PathVariable String report return ResponseEntity.status(HttpStatus.OK).body(reportResponse); } - @PostMapping("/board") - public ResponseEntity generateBoardReport(@RequestBody GenerateBoardReportRequest request) { - log.info( - "Start to generate board report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _boardReportId: {}", - request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(), request.getEndTime(), - IdUtil.getBoardReportId(request.getCsvTimeStamp())); - generateReporterService.initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics()); - CompletableFuture.runAsync(() -> { - try { - ReportResponse reportResponse = generateReporterService - .generateReporter(request.convertToReportRequest()); - generateReporterService.saveReporterInHandler(reportResponse, - IdUtil.getBoardReportId(request.getCsvTimeStamp())); - generateReporterService.updateMetricsDataReadyInHandler(request.getCsvTimeStamp(), - request.getMetrics()); - log.info( - "Successfully generate board report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _boardReportId: {}", - request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(), - request.getEndTime(), IdUtil.getBoardReportId(request.getCsvTimeStamp())); - - } - catch (BaseException e) { - asyncExceptionHandler.put(IdUtil.getBoardReportId(request.getCsvTimeStamp()), e); - } - }); - - String callbackUrl = "/reports/" + request.getCsvTimeStamp(); - return ResponseEntity.status(HttpStatus.ACCEPTED) - .body(CallbackResponse.builder().callbackUrl(callbackUrl).interval(interval).build()); - } - - @PostMapping("/dora") - public ResponseEntity generateDoraReport(@RequestBody GenerateDoraReportRequest request) { - log.info( - "Start to generate dora report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _doraReportId: {}", - request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(), request.getEndTime(), - IdUtil.getDoraReportId(request.getCsvTimeStamp())); - generateReporterService.initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics()); + @PostMapping("{reportType}") + public ResponseEntity generateReport(@PathVariable ReportType reportType, + @RequestBody GenerateReportRequest request) { CompletableFuture.runAsync(() -> { - try { - ReportResponse reportResponse = generateReporterService - .generateReporter(request.convertToReportRequest()); - generateReporterService.saveReporterInHandler(reportResponse, - IdUtil.getDoraReportId(request.getCsvTimeStamp())); - generateReporterService.updateMetricsDataReadyInHandler(request.getCsvTimeStamp(), - request.getMetrics()); - log.info( - "Successfully generate dora report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _doraReportId: {}", - request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(), - request.getEndTime(), IdUtil.getDoraReportId(request.getCsvTimeStamp())); - } - catch (BaseException e) { - asyncExceptionHandler.put(IdUtil.getDoraReportId(request.getCsvTimeStamp()), e); + switch (reportType) { + case BOARD -> generateReporterService.generateBoardReport(request); + case DORA -> generateReporterService.generateDoraReport(request); } }); - String callbackUrl = "/reports/" + request.getCsvTimeStamp(); return ResponseEntity.status(HttpStatus.ACCEPTED) .body(CallbackResponse.builder().callbackUrl(callbackUrl).interval(interval).build()); diff --git a/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateBoardReportRequest.java b/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateBoardReportRequest.java deleted file mode 100644 index 6c26273c52..0000000000 --- a/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateBoardReportRequest.java +++ /dev/null @@ -1,45 +0,0 @@ -package heartbeat.controller.report.dto.request; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public class GenerateBoardReportRequest { - - private Boolean considerHoliday; - - @NotBlank(message = "StartTime is required") - private String startTime; - - @NotBlank(message = "EndTime is required") - private String endTime; - - private List metrics; - - private JiraBoardSetting jiraBoardSetting; - - @NotBlank - private String csvTimeStamp; - - public GenerateReportRequest convertToReportRequest() { - return GenerateReportRequest.builder() - .considerHoliday(this.considerHoliday) - .startTime(this.startTime) - .endTime(this.endTime) - .metrics(this.metrics) - .jiraBoardSetting(this.jiraBoardSetting) - .csvTimeStamp(this.csvTimeStamp) - .build(); - } - -} diff --git a/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateDoraReportRequest.java b/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateDoraReportRequest.java deleted file mode 100644 index 3bb4539f8d..0000000000 --- a/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateDoraReportRequest.java +++ /dev/null @@ -1,48 +0,0 @@ -package heartbeat.controller.report.dto.request; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public class GenerateDoraReportRequest { - - private Boolean considerHoliday; - - @NotBlank(message = "StartTime is required") - private String startTime; - - @NotBlank(message = "EndTime is required") - private String endTime; - - private List metrics; - - private BuildKiteSetting buildKiteSetting; - - private CodebaseSetting codebaseSetting; - - @NotBlank - private String csvTimeStamp; - - public GenerateReportRequest convertToReportRequest() { - GenerateReportRequest reportRequest = new GenerateReportRequest(); - reportRequest.setConsiderHoliday(this.considerHoliday); - reportRequest.setStartTime(this.startTime); - reportRequest.setEndTime(this.endTime); - reportRequest.setMetrics(this.metrics); - reportRequest.setBuildKiteSetting(this.buildKiteSetting); - reportRequest.setCodebaseSetting(this.codebaseSetting); - reportRequest.setCsvTimeStamp(this.csvTimeStamp); - return reportRequest; - } - -} diff --git a/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateReportRequest.java b/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateReportRequest.java index a4fdb9f0ca..4c5977f565 100644 --- a/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateReportRequest.java +++ b/backend/src/main/java/heartbeat/controller/report/dto/request/GenerateReportRequest.java @@ -1,6 +1,8 @@ package heartbeat.controller.report.dto.request; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.google.gson.Gson; +import heartbeat.util.MetricsUtil; import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; @@ -35,4 +37,23 @@ public class GenerateReportRequest { @NotBlank private String csvTimeStamp; + public GenerateReportRequest convertToPipelineRequest(GenerateReportRequest request) { + List pipelineMetrics = MetricsUtil + .getPipelineMetrics(request.getMetrics().stream().map(String::toLowerCase).toList()); + Gson gson = new Gson(); + GenerateReportRequest result = gson.fromJson(gson.toJson(request), GenerateReportRequest.class); + + result.setMetrics(pipelineMetrics); + return result; + } + + public GenerateReportRequest convertToSourceControlRequest(GenerateReportRequest request) { + List codebaseMetrics = MetricsUtil + .getCodeBaseMetrics(request.getMetrics().stream().map(String::toLowerCase).toList()); + Gson gson = new Gson(); + GenerateReportRequest result = gson.fromJson(gson.toJson(request), GenerateReportRequest.class); + result.setMetrics(codebaseMetrics); + return result; + } + } diff --git a/backend/src/main/java/heartbeat/controller/report/dto/request/ReportType.java b/backend/src/main/java/heartbeat/controller/report/dto/request/ReportType.java new file mode 100644 index 0000000000..2ef7a05d75 --- /dev/null +++ b/backend/src/main/java/heartbeat/controller/report/dto/request/ReportType.java @@ -0,0 +1,15 @@ +package heartbeat.controller.report.dto.request; + +public enum ReportType { + + BOARD, DORA; + + public static ReportType fromValue(String type) { + return switch (type) { + case "board" -> BOARD; + case "dora" -> DORA; + default -> throw new IllegalArgumentException("ReportType not found!"); + }; + } + +} diff --git a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java index 3fa0bd27a8..ec1ca6c9a8 100644 --- a/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java +++ b/backend/src/main/java/heartbeat/service/report/GenerateReporterService.java @@ -28,7 +28,6 @@ import heartbeat.controller.report.dto.request.ExportCSVRequest; import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.request.JiraBoardSetting; -import heartbeat.controller.report.dto.request.RequireDataEnum; import heartbeat.controller.report.dto.response.BoardCSVConfig; import heartbeat.controller.report.dto.response.BoardCSVConfigEnum; import heartbeat.controller.report.dto.response.LeadTimeInfo; @@ -71,16 +70,18 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Stream; import heartbeat.util.IdUtil; +import heartbeat.util.MetricsUtil; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import lombok.val; +import org.apache.commons.collections.CollectionUtils; import org.springframework.core.io.InputStreamResource; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -127,21 +128,6 @@ public class GenerateReporterService { private final AsyncExceptionHandler asyncExceptionHandler; - private final List kanbanMetrics = Stream - .of(RequireDataEnum.VELOCITY, RequireDataEnum.CYCLE_TIME, RequireDataEnum.CLASSIFICATION) - .map(RequireDataEnum::getValue) - .toList(); - - private final List buildKiteMetrics = Stream - .of(RequireDataEnum.CHANGE_FAILURE_RATE, RequireDataEnum.DEPLOYMENT_FREQUENCY, - RequireDataEnum.MEAN_TIME_TO_RECOVERY) - .map(RequireDataEnum::getValue) - .toList(); - - private final List codebaseMetrics = Stream.of(RequireDataEnum.LEAD_TIME_FOR_CHANGES) - .map(RequireDataEnum::getValue) - .toList(); - private static StoryPointsAndCycleTimeRequest buildStoryPointsAndCycleTimeRequest(JiraBoardSetting jiraBoardSetting, String startTime, String endTime) { return StoryPointsAndCycleTimeRequest.builder() @@ -213,17 +199,94 @@ else if (jsonObject.has("key")) { return result; } + public void generateBoardReport(GenerateReportRequest request) { + initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics()); + log.info( + "Start to generate board report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _boardReportId: {}", + request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(), request.getEndTime(), + IdUtil.getBoardReportId(request.getCsvTimeStamp())); + CompletableFuture.runAsync(() -> { + try { + saveReporterInHandler(generateReporter(request), IdUtil.getBoardReportId(request.getCsvTimeStamp())); + updateMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics()); + log.info( + "Successfully generate board report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _boardReportId: {}", + request.getMetrics(), request.getConsiderHoliday(), request.getStartTime(), + request.getEndTime(), IdUtil.getBoardReportId(request.getCsvTimeStamp())); + } + catch (BaseException e) { + asyncExceptionHandler.put(IdUtil.getBoardReportId(request.getCsvTimeStamp()), e); + } + }); + } + + public void generateDoraReport(GenerateReportRequest request) { + MetricsDataReady metricsDataStatus = getMetricsStatus(request.getMetrics(), Boolean.TRUE); + initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), request.getMetrics()); + if (Objects.nonNull(metricsDataStatus.isSourceControlMetricsReady()) + && metricsDataStatus.isSourceControlMetricsReady()) { + generateSourceControlReport(request); + } + if (Objects.nonNull(metricsDataStatus.isPipelineMetricsReady()) && metricsDataStatus.isPipelineMetricsReady()) { + generatePipelineReport(request); + } + generateCsvForDora(request); + } + + private void generatePipelineReport(GenerateReportRequest request) { + GenerateReportRequest pipelineRequest = request.convertToPipelineRequest(request); + log.info( + "Start to generate pipeline report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _pipelineReportId: {}", + pipelineRequest.getMetrics(), pipelineRequest.getConsiderHoliday(), pipelineRequest.getStartTime(), + pipelineRequest.getEndTime(), IdUtil.getPipelineReportId(request.getCsvTimeStamp())); + CompletableFuture.runAsync(() -> { + try { + saveReporterInHandler(generateReporter(pipelineRequest), + IdUtil.getPipelineReportId(pipelineRequest.getCsvTimeStamp())); + updateMetricsDataReadyInHandler(pipelineRequest.getCsvTimeStamp(), pipelineRequest.getMetrics()); + log.info( + "Successfully generate pipeline report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _pipelineReportId: {}", + pipelineRequest.getMetrics(), pipelineRequest.getConsiderHoliday(), + pipelineRequest.getStartTime(), pipelineRequest.getEndTime(), + IdUtil.getPipelineReportId(request.getCsvTimeStamp())); + } + catch (BaseException e) { + asyncExceptionHandler.put(IdUtil.getPipelineReportId(request.getCsvTimeStamp()), e); + } + }); + } + + private void generateSourceControlReport(GenerateReportRequest request) { + GenerateReportRequest sourceControlRequest = request.convertToSourceControlRequest(request); + log.info( + "Start to generate source control report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _sourceControlReportId: {}", + sourceControlRequest.getMetrics(), sourceControlRequest.getConsiderHoliday(), + sourceControlRequest.getStartTime(), sourceControlRequest.getEndTime(), + IdUtil.getSourceControlReportId(request.getCsvTimeStamp())); + CompletableFuture.runAsync(() -> { + try { + saveReporterInHandler(generateReporter(sourceControlRequest), + IdUtil.getSourceControlReportId(sourceControlRequest.getCsvTimeStamp())); + updateMetricsDataReadyInHandler(sourceControlRequest.getCsvTimeStamp(), + sourceControlRequest.getMetrics()); + log.info( + "Successfully generate codebase report, _metrics: {}, _considerHoliday: {}, _startTime: {}, _endTime: {}, _sourceControlReportId: {}", + sourceControlRequest.getMetrics(), sourceControlRequest.getConsiderHoliday(), + sourceControlRequest.getStartTime(), sourceControlRequest.getEndTime(), + IdUtil.getSourceControlReportId(request.getCsvTimeStamp())); + } + catch (BaseException e) { + asyncExceptionHandler.put(IdUtil.getSourceControlReportId(request.getCsvTimeStamp()), e); + } + }); + } + public synchronized ReportResponse generateReporter(GenerateReportRequest request) { workDay.changeConsiderHolidayMode(request.getConsiderHoliday()); // fetch data for calculate List lowMetrics = request.getMetrics().stream().map(String::toLowerCase).toList(); FetchedData fetchedData = fetchOriginalData(request, lowMetrics); - if (lowMetrics.stream().anyMatch(this.codebaseMetrics::contains) - || lowMetrics.stream().anyMatch(this.buildKiteMetrics::contains)) { - generateCSVForPipeline(request, fetchedData.getBuildKiteData()); - } - ReportResponse reportResponse = new ReportResponse(EXPORT_CSV_VALIDITY_TIME); JiraBoardSetting jiraBoardSetting = request.getJiraBoardSetting(); @@ -258,21 +321,21 @@ public synchronized ReportResponse generateReporter(GenerateReportRequest reques private FetchedData fetchOriginalData(GenerateReportRequest request, List lowMetrics) { FetchedData fetchedData = new FetchedData(); - if (lowMetrics.stream().anyMatch(this.kanbanMetrics::contains)) { + if (CollectionUtils.isNotEmpty(MetricsUtil.getBoardMetrics(lowMetrics))) { if (request.getJiraBoardSetting() == null) throw new BadRequestException("Failed to fetch Jira info due to Jira board setting is null."); CardCollectionInfo cardCollectionInfo = fetchDataFromKanban(request); fetchedData.setCardCollectionInfo(cardCollectionInfo); } - if (lowMetrics.stream().anyMatch(this.codebaseMetrics::contains)) { + if (CollectionUtils.isNotEmpty(MetricsUtil.getCodeBaseMetrics(lowMetrics))) { if (request.getCodebaseSetting() == null) throw new BadRequestException("Failed to fetch Github info due to code base setting is null."); BuildKiteData buildKiteData = fetchGithubData(request); fetchedData.setBuildKiteData(buildKiteData); } - if (lowMetrics.stream().anyMatch(this.buildKiteMetrics::contains)) { + if (CollectionUtils.isNotEmpty(MetricsUtil.getPipelineMetrics(lowMetrics))) { if (request.getBuildKiteSetting() == null) throw new BadRequestException("Failed to fetch BuildKite info due to BuildKite setting is null."); FetchedData.BuildKiteData buildKiteData = fetchBuildKiteInfo(request); @@ -287,6 +350,14 @@ private FetchedData fetchOriginalData(GenerateReportRequest request, List lowMetrics = request.getMetrics().stream().map(String::toLowerCase).toList(); + FetchedData fetchedData = fetchOriginalData(request, lowMetrics); + + generateCSVForPipeline(request, fetchedData.getBuildKiteData()); + + } + private CardCollection fetchRealDoneCardCollection(GenerateReportRequest request) { JiraBoardSetting jiraBoardSetting = request.getJiraBoardSetting(); StoryPointsAndCycleTimeRequest storyPointsAndCycleTimeRequest = buildStoryPointsAndCycleTimeRequest( @@ -638,20 +709,17 @@ private Boolean checkCurrentMetricsReadyState(Boolean exist, Boolean previousVal } private MetricsDataReady getMetricsStatus(List metrics, Boolean flag) { - boolean boardMetricsExist = metrics.stream().map(String::toLowerCase).anyMatch(this.kanbanMetrics::contains); - boolean codebaseMetricsExist = metrics.stream() - .map(String::toLowerCase) - .anyMatch(this.codebaseMetrics::contains); - boolean buildKiteMetricsExist = metrics.stream() - .map(String::toLowerCase) - .anyMatch(this.buildKiteMetrics::contains); + List lowerMetrics = metrics.stream().map(String::toLowerCase).toList(); + boolean boardMetricsExist = CollectionUtils.isNotEmpty(MetricsUtil.getBoardMetrics(lowerMetrics)); + boolean codebaseMetricsExist = CollectionUtils.isNotEmpty(MetricsUtil.getCodeBaseMetrics(lowerMetrics)); + boolean buildKiteMetricsExist = CollectionUtils.isNotEmpty(MetricsUtil.getPipelineMetrics(lowerMetrics)); Boolean isBoardMetricsReady = boardMetricsExist ? flag : null; - Boolean isCodebaseMetricsReady = codebaseMetricsExist ? flag : null; - Boolean isBuildKiteMetricsReady = buildKiteMetricsExist ? flag : null; + Boolean isPipelineMetricsReady = buildKiteMetricsExist ? flag : null; + Boolean isSourceControlMetricsReady = codebaseMetricsExist ? flag : null; return MetricsDataReady.builder() .isBoardMetricsReady(isBoardMetricsReady) - .isPipelineMetricsReady(isBuildKiteMetricsReady) - .isSourceControlMetricsReady(isCodebaseMetricsReady) + .isPipelineMetricsReady(isPipelineMetricsReady) + .isSourceControlMetricsReady(isSourceControlMetricsReady) .build(); } @@ -691,10 +759,6 @@ private List generateCSVForPipelineWithCodebase(CodebaseSetting String endTime, BuildKiteData buildKiteData, List deploymentEnvironments) { List pipelineCSVInfos = new ArrayList<>(); - if (codebaseSetting == null && CollectionUtils.isEmpty(deploymentEnvironments)) { - return pipelineCSVInfos; - } - Map repoIdMap = getRepoMap(deploymentEnvironments); for (DeploymentEnvironment deploymentEnvironment : deploymentEnvironments) { String repoId = GithubUtil.getGithubUrlFullName(repoIdMap.get(deploymentEnvironment.getId())); @@ -742,7 +806,7 @@ public boolean checkGenerateReportIsDone(String reportTimeStamp) { throw new GenerateReportException("Failed to get report due to report time expires"); } BaseException boardException = asyncExceptionHandler.get(IdUtil.getBoardReportId(reportTimeStamp)); - BaseException doraException = asyncExceptionHandler.get(IdUtil.getDoraReportId(reportTimeStamp)); + BaseException doraException = asyncExceptionHandler.get(IdUtil.getPipelineReportId(reportTimeStamp)); handleAsyncException(boardException); handleAsyncException(doraException); return asyncReportRequestHandler.isReportReady(reportTimeStamp); @@ -805,7 +869,8 @@ public ReportResponse getReportFromHandler(String reportId) { public ReportResponse getComposedReportResponse(String reportId, boolean isReportReady) { ReportResponse boardReportResponse = getReportFromHandler(IdUtil.getBoardReportId(reportId)); - ReportResponse doraReportResponse = getReportFromHandler(IdUtil.getDoraReportId(reportId)); + ReportResponse doraReportResponse = getReportFromHandler(IdUtil.getPipelineReportId(reportId)); + ReportResponse codebaseReportResponse = getReportFromHandler(IdUtil.getSourceControlReportId(reportId)); MetricsDataReady metricsDataReady = asyncReportRequestHandler.getMetricsDataReady(reportId); ReportResponse response = Optional.ofNullable(boardReportResponse).orElse(doraReportResponse); @@ -817,7 +882,7 @@ public ReportResponse getComposedReportResponse(String reportId, boolean isRepor .deploymentFrequency(getValueOrNull(doraReportResponse, ReportResponse::getDeploymentFrequency)) .changeFailureRate(getValueOrNull(doraReportResponse, ReportResponse::getChangeFailureRate)) .meanTimeToRecovery(getValueOrNull(doraReportResponse, ReportResponse::getMeanTimeToRecovery)) - .leadTimeForChanges(getValueOrNull(doraReportResponse, ReportResponse::getLeadTimeForChanges)) + .leadTimeForChanges(getValueOrNull(codebaseReportResponse, ReportResponse::getLeadTimeForChanges)) .isBoardMetricsReady(getValueOrNull(metricsDataReady, MetricsDataReady::isBoardMetricsReady)) .isPipelineMetricsReady(getValueOrNull(metricsDataReady, MetricsDataReady::isPipelineMetricsReady)) .isSourceControlMetricsReady( diff --git a/backend/src/main/java/heartbeat/util/IdUtil.java b/backend/src/main/java/heartbeat/util/IdUtil.java index 6d74881644..1fdfc1952b 100644 --- a/backend/src/main/java/heartbeat/util/IdUtil.java +++ b/backend/src/main/java/heartbeat/util/IdUtil.java @@ -4,14 +4,20 @@ public interface IdUtil { String BOARD_REPORT_PREFIX = "board-"; - String DORA_REPORT_PREFIX = "dora-"; + String PIPELINE_REPORT_PREFIX = "pipeline-"; + + String SOURCE_CONTROL_PREFIX = "sourceControl-"; static String getBoardReportId(String timeStamp) { return BOARD_REPORT_PREFIX + timeStamp; } - static String getDoraReportId(String timeStamp) { - return DORA_REPORT_PREFIX + timeStamp; + static String getPipelineReportId(String timeStamp) { + return PIPELINE_REPORT_PREFIX + timeStamp; + } + + static String getSourceControlReportId(String timeStamp) { + return SOURCE_CONTROL_PREFIX + timeStamp; } static String getTimeStampFromReportId(String reportId) { diff --git a/backend/src/main/java/heartbeat/util/MetricsUtil.java b/backend/src/main/java/heartbeat/util/MetricsUtil.java new file mode 100644 index 0000000000..58d6dee5eb --- /dev/null +++ b/backend/src/main/java/heartbeat/util/MetricsUtil.java @@ -0,0 +1,39 @@ +package heartbeat.util; + +import heartbeat.controller.report.dto.request.RequireDataEnum; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public interface MetricsUtil { + + List kanbanMetrics = Stream + .of(RequireDataEnum.VELOCITY, RequireDataEnum.CYCLE_TIME, RequireDataEnum.CLASSIFICATION) + .map(RequireDataEnum::getValue) + .toList(); + + List buildKiteMetrics = Stream + .of(RequireDataEnum.CHANGE_FAILURE_RATE, RequireDataEnum.DEPLOYMENT_FREQUENCY, + RequireDataEnum.MEAN_TIME_TO_RECOVERY) + .map(RequireDataEnum::getValue) + .toList(); + + List codebaseMetrics = Stream.of(RequireDataEnum.LEAD_TIME_FOR_CHANGES) + .map(RequireDataEnum::getValue) + .toList(); + + static List getPipelineMetrics(List metrics) { + return metrics.stream().filter(buildKiteMetrics::contains).collect(Collectors.toList()); + } + + static List getCodeBaseMetrics(List metrics) { + return metrics.stream().filter(codebaseMetrics::contains).collect(Collectors.toList()); + + } + + static List getBoardMetrics(List metrics) { + return metrics.stream().filter(kanbanMetrics::contains).collect(Collectors.toList()); + } + +} diff --git a/backend/src/test/java/heartbeat/controller/report/GenerateReporterControllerTest.java b/backend/src/test/java/heartbeat/controller/report/GenerateReporterControllerTest.java index d2b939e491..6ea0bf6a68 100644 --- a/backend/src/test/java/heartbeat/controller/report/GenerateReporterControllerTest.java +++ b/backend/src/test/java/heartbeat/controller/report/GenerateReporterControllerTest.java @@ -3,17 +3,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; import heartbeat.controller.report.dto.request.ExportCSVRequest; -import heartbeat.controller.report.dto.request.GenerateBoardReportRequest; -import heartbeat.controller.report.dto.request.GenerateDoraReportRequest; -import heartbeat.controller.report.dto.response.AvgDeploymentFrequency; -import heartbeat.controller.report.dto.response.DeploymentFrequency; +import heartbeat.controller.report.dto.request.GenerateReportRequest; import heartbeat.controller.report.dto.response.ReportResponse; -import heartbeat.controller.report.dto.response.Velocity; import heartbeat.exception.GenerateReportException; -import heartbeat.exception.RequestFailedException; import heartbeat.service.report.GenerateReporterService; import heartbeat.handler.AsyncExceptionHandler; -import heartbeat.util.IdUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -28,11 +22,9 @@ import java.io.ByteArrayInputStream; import java.io.File; - import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -141,136 +133,45 @@ void shouldReturnWhenExportCsv() throws Exception { } @Test - void shouldReturnAcceptedStatusAndCallbackUrlAndIntervalWhenCallBoardReports() throws Exception { - ReportResponse expectedResponse = ReportResponse.builder() - .velocity(Velocity.builder().velocityForSP(10).build()) - .deploymentFrequency(DeploymentFrequency.builder() - .avgDeploymentFrequency(new AvgDeploymentFrequency("Average", 0.10F)) - .build()) - .build(); - + void shouldReturnCallBackUrlWithAcceptedStatusAndInvokeGenerateBoardReportWhenReportTypeIsBoard() throws Exception { ObjectMapper mapper = new ObjectMapper(); - GenerateBoardReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), - GenerateBoardReportRequest.class); + GenerateReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), GenerateReportRequest.class); String currentTimeStamp = "1685010080107"; request.setCsvTimeStamp(currentTimeStamp); - when(generateReporterService.generateReporter(request.convertToReportRequest())).thenReturn(expectedResponse); - doNothing().when(generateReporterService).initializeMetricsDataReadyInHandler(any(), any()); - doNothing().when(generateReporterService).saveReporterInHandler(any(), any()); - doNothing().when(generateReporterService).updateMetricsDataReadyInHandler(any(), any()); - MockHttpServletResponse response = mockMvc - .perform(post("/reports/board").contentType(MediaType.APPLICATION_JSON) + .perform(post("/reports/dora").contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(request))) .andExpect(status().isAccepted()) .andReturn() .getResponse(); - final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString(); - final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString(); - assertEquals("/reports/" + currentTimeStamp, callbackUrl); - assertEquals("10", interval); - } - - @Test - void shouldGetExceptionAndPutInExceptionMapWhenCallBoardReport() throws Exception { - ObjectMapper mapper = new ObjectMapper(); - GenerateBoardReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), - GenerateBoardReportRequest.class); - String currentTimeStamp = "1685010080107"; - request.setCsvTimeStamp(currentTimeStamp); - - RequestFailedException requestFailedException = new RequestFailedException(402, "Client Error"); - when(generateReporterService.generateReporter(request.convertToReportRequest())) - .thenThrow(requestFailedException); - doNothing().when(generateReporterService).initializeMetricsDataReadyInHandler(any(), any()); - doNothing().when(generateReporterService).saveReporterInHandler(any(), any()); - doNothing().when(generateReporterService).updateMetricsDataReadyInHandler(any(), any()); - - MockHttpServletResponse response = mockMvc - .perform(post("/reports/board").contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(request))) - .andExpect(status().isAccepted()) - .andReturn() - .getResponse(); + Thread.sleep(2000); + verify(generateReporterService, times(1)).generateDoraReport(request); + verify(generateReporterService, times(0)).generateBoardReport(request); final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString(); final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString(); assertEquals("/reports/" + currentTimeStamp, callbackUrl); assertEquals("10", interval); - - Thread.sleep(2000L); - verify(generateReporterService).initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), - request.getMetrics()); - verify(generateReporterService, times(0)).saveReporterInHandler(any(), any()); - verify(generateReporterService, times(0)).updateMetricsDataReadyInHandler(request.getCsvTimeStamp(), - request.getMetrics()); - verify(asyncExceptionHandler).put(IdUtil.getBoardReportId(currentTimeStamp), requestFailedException); } @Test - void shouldGetExceptionAndPutInExceptionMapWhenCallDoraReport() throws Exception { + void shouldReturnCallBackUrlWithAcceptedStatusAndInvokeGenerateDoraReportWhenReportTypeIsBoard() throws Exception { ObjectMapper mapper = new ObjectMapper(); - GenerateDoraReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), - GenerateDoraReportRequest.class); + GenerateReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), GenerateReportRequest.class); String currentTimeStamp = "1685010080107"; request.setCsvTimeStamp(currentTimeStamp); - RequestFailedException requestFailedException = new RequestFailedException(402, "Client Error"); - when(generateReporterService.generateReporter(request.convertToReportRequest())) - .thenThrow(requestFailedException); - doNothing().when(generateReporterService).initializeMetricsDataReadyInHandler(any(), any()); - doNothing().when(generateReporterService).saveReporterInHandler(any(), any()); - doNothing().when(generateReporterService).updateMetricsDataReadyInHandler(any(), any()); - MockHttpServletResponse response = mockMvc - .perform(post("/reports/dora").contentType(MediaType.APPLICATION_JSON) + .perform(post("/reports/board").contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(request))) .andExpect(status().isAccepted()) .andReturn() .getResponse(); - final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString(); - final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString(); - assertEquals("/reports/" + currentTimeStamp, callbackUrl); - assertEquals("10", interval); - - Thread.sleep(2000L); - verify(generateReporterService).initializeMetricsDataReadyInHandler(request.getCsvTimeStamp(), - request.getMetrics()); - verify(generateReporterService, times(0)).saveReporterInHandler(any(), any()); - verify(generateReporterService, times(0)).updateMetricsDataReadyInHandler(request.getCsvTimeStamp(), - request.getMetrics()); - verify(asyncExceptionHandler).put(IdUtil.getDoraReportId(currentTimeStamp), requestFailedException); - } - - @Test - void shouldReturnAcceptedStatusAndCallbackUrlAndIntervalWhenCallDoraReports() throws Exception { - ReportResponse expectedResponse = ReportResponse.builder() - .deploymentFrequency(DeploymentFrequency.builder() - .avgDeploymentFrequency(new AvgDeploymentFrequency("Average", 0.10F)) - .build()) - .velocity(Velocity.builder().velocityForSP(10).build()) - .deploymentFrequency(DeploymentFrequency.builder() - .avgDeploymentFrequency(new AvgDeploymentFrequency("Average", 0.10F)) - .build()) - .build(); - - ObjectMapper mapper = new ObjectMapper(); - GenerateBoardReportRequest request = mapper.readValue(new File(REQUEST_FILE_PATH), - GenerateBoardReportRequest.class); - String currentTimeStamp = "1685010080107"; - request.setCsvTimeStamp(currentTimeStamp); - - when(generateReporterService.generateReporter(request.convertToReportRequest())).thenReturn(expectedResponse); - - MockHttpServletResponse response = mockMvc - .perform(post("/reports/dora").contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(request))) - .andExpect(status().isAccepted()) - .andReturn() - .getResponse(); + verify(generateReporterService, times(0)).generateDoraReport(request); + verify(generateReporterService, times(1)).generateBoardReport(request); final var callbackUrl = JsonPath.parse(response.getContentAsString()).read("$.callbackUrl").toString(); final var interval = JsonPath.parse(response.getContentAsString()).read("$.interval").toString(); diff --git a/backend/src/test/java/heartbeat/controller/report/dto/request/ReportTypeTest.java b/backend/src/test/java/heartbeat/controller/report/dto/request/ReportTypeTest.java new file mode 100644 index 0000000000..1966306d96 --- /dev/null +++ b/backend/src/test/java/heartbeat/controller/report/dto/request/ReportTypeTest.java @@ -0,0 +1,25 @@ +package heartbeat.controller.report.dto.request; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ReportTypeTest { + + @Test + public void shouldConvertValueToType() { + ReportType boardType = ReportType.fromValue("board"); + ReportType doraType = ReportType.fromValue("dora"); + + assertEquals(boardType, ReportType.BOARD); + assertEquals(doraType, ReportType.DORA); + } + + @Test + public void shouldThrowExceptionWhenDateTypeNotSupported() { + assertThatThrownBy(() -> ReportType.fromValue("unknown")).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ReportType not found!"); + } + +} diff --git a/backend/src/test/java/heartbeat/controller/report/request.json b/backend/src/test/java/heartbeat/controller/report/request.json index e35ab296be..4b30ad4d09 100644 --- a/backend/src/test/java/heartbeat/controller/report/request.json +++ b/backend/src/test/java/heartbeat/controller/report/request.json @@ -9,6 +9,7 @@ ], "startTime": 1661702400000, "endTime": 1662739199000, + "considerHoliday": true, "buildKiteSetting": { "type": "BuildKite", "token": "0d6*********************************a57a", @@ -36,6 +37,11 @@ } ] }, + "codebaseSetting": { + "type": "GitHub", + "token": "ghp*********************************FX", + "leadTime": [] + }, "jiraBoardSetting": { "type": "classic jira", "token": "Basic exxxxxhhbmdxxxxvYixxxxxxZzlxxxxxx3Rqaxxxxxlodxxxxx9BRUU3", diff --git a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java index 571d69a7ed..0a7e24dd12 100644 --- a/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java +++ b/backend/src/test/java/heartbeat/service/report/GenerateReporterServiceTest.java @@ -103,13 +103,16 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; + +import static heartbeat.TestFixtures.BUILDKITE_TOKEN; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - -import static heartbeat.TestFixtures.BUILDKITE_TOKEN; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.doThrow; import static org.mockito.internal.verification.VerificationModeFactory.times; @ExtendWith(MockitoExtension.class) @@ -118,12 +121,19 @@ class GenerateReporterServiceTest { public static final String SITE_ATLASSIAN_NET = "https://site.atlassian.net"; + private static final String REQUEST_FILE_PATH = "src/test/java/heartbeat/controller/report/request.json"; + + private static final String RESPONSE_FILE_PATH = "src/test/java/heartbeat/controller/report/reportResponse.json"; + @InjectMocks GenerateReporterService generateReporterService; @Mock JiraService jiraService; + @Mock + IdUtil idUtil; + @Mock WorkDay workDay; @@ -577,7 +587,7 @@ void shouldGenerateCsvForPipelineWithPipelineMetricAndBuildInfoIsEmpty() throws return null; }).when(csvFileGenerator).convertPipelineDataToCSV(any(), any()); - generateReporterService.generateReporter(request); + generateReporterService.generateDoraReport(request); boolean isExists = Files.exists(csvFilePath); Assertions.assertTrue(isExists); @@ -627,7 +637,7 @@ void shouldGenerateCsvForPipelineWithPipelineMetric() throws IOException { return null; }).when(csvFileGenerator).convertPipelineDataToCSV(any(), any()); - generateReporterService.generateReporter(request); + generateReporterService.generateDoraReport(request); boolean isExists = Files.exists(mockPipelineCsvPath); Assertions.assertTrue(isExists); @@ -673,7 +683,7 @@ void shouldNotGenerateCsvForPipelineWithPipelineLeadTimeIsNull() throws IOExcept when(gitHubService.fetchPipelinesLeadTime(any(), any(), any())).thenReturn(null); when(buildKiteService.getStepsBeforeEndStep(any(), any())).thenReturn(List.of("xx")); - generateReporterService.generateReporter(request); + generateReporterService.generateDoraReport(request); boolean isExists = Files.exists(mockPipelineCsvPath); Assertions.assertFalse(isExists); @@ -718,7 +728,7 @@ void shouldNotGenerateCsvForPipelineWithCommitIsNull() throws IOException { when(gitHubService.fetchPipelinesLeadTime(any(), any(), any())).thenReturn(null); when(buildKiteService.getStepsBeforeEndStep(any(), any())).thenReturn(List.of("xx")); - generateReporterService.generateReporter(request); + generateReporterService.generateDoraReport(request); boolean isExists = Files.exists(mockPipelineCsvPath); Assertions.assertFalse(isExists); @@ -762,7 +772,7 @@ void shouldNotGenerateCsvForPipeline() throws IOException { when(gitHubService.fetchPipelinesLeadTime(any(), any(), any())).thenReturn(null); when(buildKiteService.getStepsBeforeEndStep(any(), any())).thenReturn(List.of("xx")); - generateReporterService.generateReporter(request); + generateReporterService.generateDoraReport(request); boolean isExists = Files.exists(mockPipelineCsvPath); Assertions.assertFalse(isExists); @@ -958,7 +968,7 @@ void shouldReturnReportResponse() { @Test void shouldThrowUnauthorizedExceptionWhenCheckGenerateReportIsDone() { String timeStamp = Long.toString(System.currentTimeMillis()); - String reportId = IdUtil.getDoraReportId(timeStamp); + String reportId = IdUtil.getPipelineReportId(timeStamp); when(asyncExceptionHandler.get(reportId)) .thenReturn(new UnauthorizedException("Failed to get GitHub info_status: 401, reason: PermissionDeny")); @@ -973,7 +983,7 @@ void shouldThrowUnauthorizedExceptionWhenCheckGenerateReportIsDone() { @Test void shouldThrowPermissionDenyExceptionWhenCheckGenerateReportIsDone() { String timeStamp = Long.toString(System.currentTimeMillis()); - String reportId = IdUtil.getDoraReportId(timeStamp); + String reportId = IdUtil.getPipelineReportId(timeStamp); asyncExceptionHandler.put(reportId, new PermissionDenyException("Failed to get GitHub info_status: 403, reason: PermissionDeny")); @@ -989,7 +999,7 @@ void shouldThrowPermissionDenyExceptionWhenCheckGenerateReportIsDone() { @Test void shouldThrowNotFoundExceptionWhenCheckGenerateReportIsDone() { String timeStamp = Long.toString(System.currentTimeMillis()); - String reportId = IdUtil.getDoraReportId(timeStamp); + String reportId = IdUtil.getPipelineReportId(timeStamp); when(asyncExceptionHandler.get(reportId)) .thenReturn(new NotFoundException("Failed to get GitHub info_status: 404, reason: NotFound")); @@ -1003,7 +1013,7 @@ void shouldThrowNotFoundExceptionWhenCheckGenerateReportIsDone() { @Test void shouldThrowGenerateReportExceptionWhenCheckGenerateReportIsDone() { String timeStamp = Long.toString(System.currentTimeMillis()); - String reportId = IdUtil.getDoraReportId(timeStamp); + String reportId = IdUtil.getPipelineReportId(timeStamp); when(asyncExceptionHandler.get(reportId)) .thenReturn(new GenerateReportException("Failed to get GitHub info_status: 500, reason: GenerateReport")); @@ -1017,7 +1027,7 @@ void shouldThrowGenerateReportExceptionWhenCheckGenerateReportIsDone() { @Test void shouldThrowServiceUnavailableExceptionWhenCheckGenerateReportIsDone() { String timeStamp = Long.toString(System.currentTimeMillis()); - String reportId = IdUtil.getDoraReportId(timeStamp); + String reportId = IdUtil.getPipelineReportId(timeStamp); when(asyncExceptionHandler.get(reportId)).thenReturn( new ServiceUnavailableException("Failed to get GitHub info_status: 503, reason: ServiceUnavailable")); @@ -1031,7 +1041,7 @@ void shouldThrowServiceUnavailableExceptionWhenCheckGenerateReportIsDone() { @Test void shouldThrowRequestFailedExceptionWhenCheckGenerateReportIsDone() { String timeStamp = Long.toString(System.currentTimeMillis()); - String reportId = IdUtil.getDoraReportId(timeStamp); + String reportId = IdUtil.getPipelineReportId(timeStamp); when(asyncExceptionHandler.get(reportId)).thenReturn(new RequestFailedException(405, "RequestFailedException")); BaseException exception = assertThrows(RequestFailedException.class, @@ -1197,10 +1207,10 @@ void shouldReturnComposedReportResponseWhenBothBoardResponseAndDoraResponseReady String timeStamp = "1683734399999"; String boardTimeStamp = "board-1683734399999"; - String doraTimestamp = "dora-1683734399999"; + String pipelineTimestamp = "pipeline-1683734399999"; when(generateReporterService.getReportFromHandler(boardTimeStamp)).thenReturn(boardResponse); - when(generateReporterService.getReportFromHandler(doraTimestamp)).thenReturn(pipelineResponse); + when(generateReporterService.getReportFromHandler(pipelineTimestamp)).thenReturn(pipelineResponse); when(asyncReportRequestHandler.getMetricsDataReady(timeStamp)) .thenReturn(new MetricsDataReady(Boolean.TRUE, Boolean.TRUE, null)); @@ -1254,8 +1264,8 @@ void shouldDoConvertMetricDataToCSVWhenCallGenerateCSVForMetrics() throws IOExce @Test void shouldPutReportInHandlerWhenCallSaveReporterInHandler() throws IOException { - String timeStamp = "1683734399999"; - String reportId = IdUtil.getDoraReportId(timeStamp); + String timeStamp = "20240109232359"; + String reportId = IdUtil.getPipelineReportId(timeStamp); ObjectMapper mapper = new ObjectMapper(); ReportResponse reportResponse = mapper .readValue(new File("src/test/java/heartbeat/controller/report/reportResponse.json"), ReportResponse.class); @@ -1266,6 +1276,126 @@ void shouldPutReportInHandlerWhenCallSaveReporterInHandler() throws IOException verify(asyncReportRequestHandler, times(1)).putReport(reportId, reportResponse); } + @Test + void shouldPutBoardReportIntoHandlerWhenCallGenerateBoardReport() throws IOException, InterruptedException { + ObjectMapper mapper = new ObjectMapper(); + GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH), + GenerateReportRequest.class); + ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class); + reportRequest.setCsvTimeStamp("20240109232359"); + MetricsDataReady previousMetricsReady = MetricsDataReady.builder() + .isBoardMetricsReady(true) + .isPipelineMetricsReady(false) + .isSourceControlMetricsReady(false) + .build(); + GenerateReporterService spyGenerateReporterService = spy(generateReporterService); + + doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest); + when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp())) + .thenReturn(previousMetricsReady); + + spyGenerateReporterService.generateBoardReport(reportRequest); + + Thread.sleep(2000); + verify(spyGenerateReporterService, times(1)).generateReporter(reportRequest); + verify(spyGenerateReporterService, times(1)) + .initializeMetricsDataReadyInHandler(reportRequest.getCsvTimeStamp(), reportRequest.getMetrics()); + verify(spyGenerateReporterService, times(1)).saveReporterInHandler( + spyGenerateReporterService.generateReporter(reportRequest), reportRequest.getCsvTimeStamp()); + verify(spyGenerateReporterService, times(1)).updateMetricsDataReadyInHandler(reportRequest.getCsvTimeStamp(), + reportRequest.getMetrics()); + } + + @Test + void shouldPutExceptionInHandlerWhenCallGenerateBoardReportThrowException() + throws IOException, InterruptedException { + ObjectMapper mapper = new ObjectMapper(); + GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH), + GenerateReportRequest.class); + ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class); + reportRequest.setCsvTimeStamp("20240109232359"); + String boardTimeStamp = "board-20240109232359"; + GenerateReporterService spyGenerateReporterService = spy(generateReporterService); + GenerateReportException e = new GenerateReportException( + "Failed to update metrics data ready through this timestamp."); + + doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest); + doThrow(e).when(spyGenerateReporterService).updateMetricsDataReadyInHandler(any(), any()); + when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp())).thenReturn(null); + + spyGenerateReporterService.generateBoardReport(reportRequest); + + Thread.sleep(2000L); + verify(spyGenerateReporterService, times(1)) + .initializeMetricsDataReadyInHandler(reportRequest.getCsvTimeStamp(), reportRequest.getMetrics()); + verify(spyGenerateReporterService, times(1)) + .saveReporterInHandler(spyGenerateReporterService.generateReporter(reportRequest), boardTimeStamp); + verify(asyncExceptionHandler, times(1)).put(boardTimeStamp, e); + } + + @Test + void shouldGeneratePipelineReportAndUpdatePipelineMetricsReadyWhenCallGeneratePipelineReport() + throws IOException, InterruptedException { + ObjectMapper mapper = new ObjectMapper(); + GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH), + GenerateReportRequest.class); + reportRequest.setMetrics(List.of("Deployment frequency")); + ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class); + GenerateReporterService spyGenerateReporterService = spy(generateReporterService); + reportRequest.setCsvTimeStamp("20240109232359"); + String doraTimeStamp = "dora-20240109232359"; + MetricsDataReady previousMetricsReady = MetricsDataReady.builder() + .isBoardMetricsReady(null) + .isPipelineMetricsReady(false) + .isSourceControlMetricsReady(false) + .build(); + + doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest); + when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp())) + .thenReturn(previousMetricsReady); + + spyGenerateReporterService.generateDoraReport(reportRequest); + + Thread.sleep(2000); + verify(spyGenerateReporterService, times(1)).generateReporter(any()); + verify(spyGenerateReporterService, times(1)).initializeMetricsDataReadyInHandler(any(), any()); + verify(spyGenerateReporterService, times(1)) + .saveReporterInHandler(spyGenerateReporterService.generateReporter(any()), doraTimeStamp); + verify(spyGenerateReporterService, times(1)).updateMetricsDataReadyInHandler(any(), any()); + } + + @Test + void shouldGenerateCodebaseReportAndUpdateCodebaseMetricsReadyWhenCallGeneratePipelineReport() + throws IOException, InterruptedException { + ObjectMapper mapper = new ObjectMapper(); + GenerateReportRequest reportRequest = mapper.readValue(new File(REQUEST_FILE_PATH), + GenerateReportRequest.class); + reportRequest.setMetrics(List.of("Lead time for changes")); + ReportResponse reportResponse = mapper.readValue(new File(RESPONSE_FILE_PATH), ReportResponse.class); + GenerateReporterService spyGenerateReporterService = spy(generateReporterService); + reportRequest.setCsvTimeStamp("20240109232359"); + String codebaseTimeStamp = "github-20240109232359"; + MetricsDataReady previousMetricsReady = MetricsDataReady.builder() + .isBoardMetricsReady(null) + .isPipelineMetricsReady(false) + .isSourceControlMetricsReady(false) + .build(); + + doReturn(reportResponse).when(spyGenerateReporterService).generateReporter(reportRequest); + when(asyncReportRequestHandler.getMetricsDataReady(reportRequest.getCsvTimeStamp())) + .thenReturn(previousMetricsReady); + + spyGenerateReporterService.generateDoraReport(reportRequest); + + Thread.sleep(2000); + verify(spyGenerateReporterService, times(1)).generateReporter(any()); + verify(spyGenerateReporterService, times(1)).initializeMetricsDataReadyInHandler(any(), any()); + verify(spyGenerateReporterService, times(1)) + .saveReporterInHandler(spyGenerateReporterService.generateReporter(any()), codebaseTimeStamp); + verify(spyGenerateReporterService, times(1)).updateMetricsDataReadyInHandler(any(), any()); + + } + private JiraBoardSetting buildJiraBoardSetting() { return JiraBoardSetting.builder() .treatFlagCardAsBlock(true) diff --git a/docs/src/content/docs/en/designs/refinement-on-generate-report.mdx b/docs/src/content/docs/en/designs/refinement-on-generate-report.mdx index 90b90d5747..88f027ed60 100644 --- a/docs/src/content/docs/en/designs/refinement-on-generate-report.mdx +++ b/docs/src/content/docs/en/designs/refinement-on-generate-report.mdx @@ -269,7 +269,7 @@ end + Async generate board report ``` -POST /reports/board +POST /reports/{board} Request payload: { @@ -319,7 +319,7 @@ Response: ``` + Async generate dora report ``` -POST /reports/dora +POST /reports/{dora} Request payload: {