From 529f5cd10a7fa051eb34222f7a65f3bf34cde89c Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 4 Sep 2025 17:37:28 +0200 Subject: [PATCH] Reformat all files Using the newly set up code formatting infrastructure --- .../aaaaaaah/velcom/backend/GlobalConfig.java | 453 +++-- .../KnownHostsIgnoringSshdFactory.java | 44 +- .../backend/RunnerAwareServerFactory.java | 272 ++- .../aaaaaaah/velcom/backend/ServerMain.java | 542 +++--- .../velcom/backend/access/AccessUtils.java | 88 +- .../archiveaccess/ArchiveReadAccess.java | 285 ++- .../exceptions/TarRetrieveException.java | 59 +- .../exceptions/TarTransferException.java | 59 +- .../benchmarkaccess/BenchmarkReadAccess.java | 744 ++++---- .../benchmarkaccess/BenchmarkWriteAccess.java | 293 ++-- .../builder/NewMeasurement.java | 131 +- .../benchmarkaccess/builder/NewRun.java | 263 +-- .../benchmarkaccess/builder/RunBuilder.java | 340 ++-- .../benchmarkaccess/entities/Measurement.java | 83 +- .../entities/MeasurementError.java | 69 +- .../entities/MeasurementValues.java | 191 +- .../access/benchmarkaccess/entities/Run.java | 182 +- .../benchmarkaccess/entities/RunError.java | 78 +- .../entities/RunErrorType.java | 75 +- .../benchmarkaccess/entities/RunId.java | 109 +- .../entities/SearchRunDescription.java | 118 +- .../entities/ShortRunDescription.java | 140 +- .../entities/sources/CommitSource.java | 102 +- .../entities/sources/TarSource.java | 85 +- .../exceptions/NoSuchRunException.java | 28 +- .../caches/AvailableDimensionsCache.java | 83 +- .../backend/access/caches/LatestRunCache.java | 168 +- .../backend/access/caches/RunCache.java | 75 +- .../committaccess/CommitReadAccess.java | 1039 +++++------ .../access/committaccess/entities/Commit.java | 233 ++- .../committaccess/entities/CommitHash.java | 69 +- .../committaccess/entities/FullCommit.java | 53 +- .../exceptions/NoSuchCommitException.java | 26 +- .../dimensionaccess/DimensionReadAccess.java | 441 +++-- .../dimensionaccess/DimensionWriteAccess.java | 82 +- .../dimensionaccess/entities/Dimension.java | 92 +- .../entities/DimensionInfo.java | 160 +- .../entities/Interpretation.java | 111 +- .../access/dimensionaccess/entities/Unit.java | 72 +- .../exceptions/NoSuchDimensionException.java | 28 +- .../access/repoaccess/RepoReadAccess.java | 261 ++- .../access/repoaccess/RepoWriteAccess.java | 298 ++-- .../access/repoaccess/entities/Branch.java | 101 +- .../repoaccess/entities/BranchName.java | 119 +- .../access/repoaccess/entities/RemoteUrl.java | 87 +- .../access/repoaccess/entities/Repo.java | 305 ++-- .../access/repoaccess/entities/RepoId.java | 113 +- .../entities/SearchBranchDescription.java | 118 +- .../exceptions/FailedToAddRepoException.java | 39 +- .../exceptions/NoSuchRepoException.java | 20 +- .../access/taskaccess/TaskReadAccess.java | 120 +- .../access/taskaccess/TaskWriteAccess.java | 545 +++--- .../access/taskaccess/entities/Task.java | 235 +-- .../access/taskaccess/entities/TaskId.java | 109 +- .../taskaccess/entities/TaskPriority.java | 85 +- .../exceptions/NoSuchTaskException.java | 28 +- .../exceptions/TaskCreationException.java | 16 +- .../backend/data/benchrepo/BenchRepo.java | 86 +- .../velcom/backend/data/queue/Policy.java | 212 +-- .../velcom/backend/data/queue/Queue.java | 391 +++-- .../data/recentruns/SignificantRun.java | 35 +- .../recentruns/SignificantRunsCollector.java | 313 ++-- .../runcomparison/DimensionDifference.java | 111 +- .../data/runcomparison/RunComparator.java | 89 +- .../data/runcomparison/RunComparison.java | 56 +- .../significance/SignificanceDetector.java | 126 +- .../significance/SignificanceFactors.java | 66 +- .../significance/SignificanceReasons.java | 83 +- .../listener/InvalidRemoteUrlException.java | 31 +- .../velcom/backend/listener/Listener.java | 653 +++---- .../listener/SynchronizeCommitsException.java | 3 +- .../listener/commits/DbUpdateException.java | 10 +- .../backend/listener/commits/DbUpdater.java | 678 ++++---- .../github/FinishedGithubCommand.java | 58 +- .../listener/github/GithubApiError.java | 16 +- .../listener/github/GithubCommand.java | 187 +- .../listener/github/GithubCommandState.java | 40 +- .../listener/github/GithubPrInteractor.java | 1548 +++++++++-------- .../listener/github/JsonBodyHandler.java | 25 +- .../listener/jgitutils/JgitCommit.java | 112 +- .../listener/jgitutils/JgitCommitWalk.java | 124 +- .../aaaaaaah/velcom/backend/package-info.java | 4 +- .../backend/restapi/authentication/Admin.java | 11 +- .../authentication/AdminAuthenticator.java | 26 +- .../endpoints/AllDimensionsEndpoint.java | 96 +- .../restapi/endpoints/AllReposEndpoint.java | 111 +- .../restapi/endpoints/CommitEndpoint.java | 213 ++- .../restapi/endpoints/CompareEndpoint.java | 200 ++- .../restapi/endpoints/DebugEndpoint.java | 193 +- .../restapi/endpoints/DimensionsEndpoint.java | 31 +- .../endpoints/GraphComparisonEndpoint.java | 356 ++-- .../endpoints/GraphDetailEndpoint.java | 394 +++-- .../GraphStatusComparisonEndpoint.java | 186 +- .../restapi/endpoints/ListenerEndpoint.java | 20 +- .../restapi/endpoints/QueueEndpoint.java | 593 +++---- .../restapi/endpoints/RecentRunsEndpoint.java | 223 +-- .../restapi/endpoints/RepoEndpoint.java | 469 ++--- .../restapi/endpoints/RunEndpoint.java | 306 ++-- .../restapi/endpoints/SearchEndpoint.java | 245 ++- .../restapi/endpoints/TestTokenEndpoint.java | 28 +- .../utils/ArgumentParseException.java | 10 +- .../endpoints/utils/EndpointUtils.java | 436 ++--- .../ArgumentParseExceptionMapper.java | 33 +- .../InvalidQueryParamsException.java | 10 +- .../InvalidQueryParamsExceptionMapper.java | 33 +- .../NoSuchCommitExceptionMapper.java | 75 +- .../NoSuchDimensionExceptionMapper.java | 75 +- .../exception/NoSuchRepoExceptionMapper.java | 58 +- .../exception/NoSuchRunExceptionMapper.java | 58 +- .../exception/NoSuchTaskExceptionMapper.java | 58 +- .../exception/TaskAlreadyExistsException.java | 30 +- .../TaskAlreadyExistsExceptionMapper.java | 81 +- .../restapi/jsonobjects/JsonBranch.java | 55 +- .../restapi/jsonobjects/JsonCommit.java | 197 ++- .../jsonobjects/JsonCommitDescription.java | 101 +- .../restapi/jsonobjects/JsonDimension.java | 87 +- .../jsonobjects/JsonDimensionDifference.java | 109 +- .../restapi/jsonobjects/JsonDimensionId.java | 30 +- .../restapi/jsonobjects/JsonMeasurement.java | 198 ++- .../backend/restapi/jsonobjects/JsonRepo.java | 90 +- .../restapi/jsonobjects/JsonResult.java | 151 +- .../backend/restapi/jsonobjects/JsonRun.java | 113 +- .../jsonobjects/JsonRunDescription.java | 78 +- .../restapi/jsonobjects/JsonRunner.java | 98 +- .../jsonobjects/JsonShortRunDescription.java | 88 +- .../restapi/jsonobjects/JsonSource.java | 159 +- .../backend/restapi/jsonobjects/JsonTask.java | 82 +- .../logging/MetricsEndpointFilter.java | 26 +- .../velcom/backend/runner/Delays.java | 36 +- .../velcom/backend/runner/Dispatcher.java | 307 ++-- .../velcom/backend/runner/IDispatcher.java | 24 +- .../velcom/backend/runner/KnownRunner.java | 314 ++-- .../runner/ServerMasterWebsocketServlet.java | 164 +- .../single/PeriodicStatusRequester.java | 240 ++- .../runner/single/RunnerConnection.java | 414 +++-- .../runner/single/TeleBinaryOutputStream.java | 67 +- .../backend/runner/single/TeleRunner.java | 760 ++++---- .../single/state/AwaitAbortRunReply.java | 32 +- .../single/state/AwaitClearResultReply.java | 77 +- .../single/state/AwaitGetResultReply.java | 77 +- .../single/state/AwaitGetStatusReply.java | 83 +- .../runner/single/state/IdleState.java | 18 +- .../runner/single/state/TeleRunnerState.java | 90 +- .../runner/single/state/TimeoutState.java | 32 +- .../velcom/backend/storage/ManagedDirs.java | 201 +-- .../backend/storage/db/DBReadAccess.java | 28 +- .../backend/storage/db/DBWriteAccess.java | 44 +- .../backend/storage/db/DatabaseStorage.java | 293 ++-- .../backend/storage/repo/GuickCloning.java | 458 +++-- .../backend/storage/repo/RepoStorage.java | 378 ++-- .../backend/storage/repo/RepositoryLock.java | 895 +++++----- .../exception/AddRepositoryException.java | 57 +- .../DirectoryAlreadyExistsException.java | 35 +- .../exception/NoSuchRepositoryException.java | 51 +- .../RepositoryAcquisitionException.java | 57 +- .../backend/storage/tar/TarFileStorage.java | 205 ++- .../velcom/backend/util/CheckedConsumer.java | 15 +- .../velcom/backend/util/CheckedFunction.java | 17 +- .../velcom/backend/util/TransferUtils.java | 206 ++- .../aaaaaaah/velcom/backend/TestCommit.java | 156 +- .../de/aaaaaaah/velcom/backend/TestDb.java | 437 ++--- .../de/aaaaaaah/velcom/backend/TestRepo.java | 200 ++- .../backend/access/AccessUtilsTest.java | 48 +- .../velcom/backend/access/GitCloneTest.java | 176 +- .../BenchmarkReadAccessTest.java | 895 +++++----- .../BenchmarkWriteAccessTest.java | 521 +++--- .../committaccess/CommitReadAccessTest.java | 1390 +++++++++------ .../committaccess/entities/CommitTest.java | 215 ++- .../DimensionReadAccessTest.java | 388 ++--- .../entities/DimensionInfoTest.java | 98 +- .../entities/DimensionTest.java | 56 +- .../entities/InterpretationTest.java | 64 +- .../access/repoaccess/RepoReadAccessTest.java | 364 ++-- .../repoaccess/RepoWriteAccessTest.java | 382 ++-- .../access/repoaccess/entities/RepoTest.java | 135 +- .../velcom/backend/data/queue/PolicyTest.java | 306 ++-- .../github/GithubPrInteractorTest.java | 145 +- .../endpoints/utils/EndpointUtilsTest.java | 31 +- .../JsonCommitDescriptionTest.java | 37 +- .../restapi/jsonobjects/JsonCommitTest.java | 71 +- .../jsonobjects/JsonDimensionInfoTest.java | 29 +- .../jsonobjects/JsonMeasurementTest.java | 124 +- .../restapi/jsonobjects/JsonRepoTest.java | 66 +- .../restapi/jsonobjects/JsonResultTest.java | 70 +- .../jsonobjects/JsonRunDescriptionTest.java | 42 +- .../restapi/jsonobjects/JsonRunTest.java | 55 +- .../restapi/jsonobjects/JsonSourceTest.java | 94 +- .../restapi/jsonobjects/JsonTaskTest.java | 39 +- .../restapi/jsonobjects/SerializingTest.java | 27 +- .../velcom/backend/runner/DispatcherTest.java | 244 ++- .../ServerMasterWebsocketServletTest.java | 257 ++- .../single/PeriodicStatusRequesterTest.java | 351 ++-- .../backend/runner/single/TeleRunnerTest.java | 422 ++--- .../storage/db/DatabaseStorageTest.java | 207 +-- .../backend/storage/repo/RepoStorageTest.java | 287 ++- .../de/aaaaaaah/velcom/runner/Connection.java | 439 +++-- .../de/aaaaaaah/velcom/runner/Delays.java | 58 +- .../aaaaaaah/velcom/runner/RunnerCliSpec.java | 57 +- .../aaaaaaah/velcom/runner/RunnerConfig.java | 82 +- .../de/aaaaaaah/velcom/runner/RunnerMain.java | 202 +-- .../aaaaaaah/velcom/runner/TeleBackend.java | 626 ++++--- .../runner/benchmarking/BenchResult.java | 110 +- .../runner/benchmarking/Benchmarker.java | 562 +++--- .../runner/benchmarking/LinuxSignal.java | 144 +- .../output/BenchmarkScriptOutputParser.java | 333 ++-- .../output/OutputParseException.java | 16 +- .../velcom/runner/formatting/NamedRows.java | 160 +- .../runner/formatting/NamedSections.java | 185 +- .../velcom/runner/states/AwaitingBench.java | 127 +- .../states/AwaitingRequestRunReply.java | 103 +- .../velcom/runner/states/AwaitingRun.java | 113 +- .../aaaaaaah/velcom/runner/states/Idle.java | 14 +- .../velcom/runner/states/RunnerState.java | 284 +-- .../velcom/runner/tmpdirs/BenchRepoDir.java | 151 +- .../velcom/runner/tmpdirs/TaskRepoDir.java | 40 +- .../velcom/runner/GitPropertiesTest.java | 22 +- .../runner/benchmarking/BenchmarkerTest.java | 615 ++++--- .../BenchmarkScriptOutputParserTest.java | 322 ++-- .../runner/formatting/NamedRowsTest.java | 110 +- .../runner/formatting/NamedSectionsTest.java | 249 +-- .../runner/tmpdirs/BenchRepoDirTest.java | 89 +- .../aaaaaaah/velcom/shared/GitProperties.java | 44 +- .../shared/protocol/HeartbeatHandler.java | 159 +- .../protocol/RunnerConnectionHeader.java | 30 +- .../shared/protocol/RunnerDenyReason.java | 60 +- .../velcom/shared/protocol/StatusCode.java | 100 +- .../shared/protocol/serialization/Result.java | 375 ++-- .../protocol/serialization/Serializer.java | 124 +- .../shared/protocol/serialization/Status.java | 22 +- .../serialization/clientbound/AbortRun.java | 11 +- .../clientbound/ClearResult.java | 12 +- .../clientbound/ClientBound.java | 18 +- .../clientbound/ClientBoundPacket.java | 76 +- .../clientbound/ClientBoundPacketType.java | 84 +- .../serialization/clientbound/GetResult.java | 11 +- .../serialization/clientbound/GetStatus.java | 15 +- .../clientbound/RequestRunReply.java | 139 +- .../serverbound/AbortRunReply.java | 12 +- .../serverbound/ClearResultReply.java | 16 +- .../serverbound/GetResultReply.java | 140 +- .../serverbound/GetStatusReply.java | 176 +- .../serialization/serverbound/RequestRun.java | 11 +- .../serverbound/ServerBound.java | 19 +- .../serverbound/ServerBoundPacket.java | 89 +- .../serverbound/ServerBoundPacketType.java | 97 +- .../shared/protocol/statemachine/State.java | 46 +- .../protocol/statemachine/StateMachine.java | 218 ++- .../aaaaaaah/velcom/shared/util/Either.java | 251 ++- .../velcom/shared/util/ExceptionHelper.java | 26 +- .../velcom/shared/util/FileHelper.java | 87 +- .../velcom/shared/util/LinesWithOffset.java | 38 +- .../aaaaaaah/velcom/shared/util/OSCheck.java | 19 +- .../de/aaaaaaah/velcom/shared/util/Pair.java | 81 +- .../velcom/shared/util/StringHelper.java | 37 +- .../shared/util/StringOutputStream.java | 62 +- .../aaaaaaah/velcom/shared/util/Timeout.java | 92 +- .../util/compression/PermissionsHelper.java | 107 +- .../shared/util/compression/TarHelper.java | 63 +- .../util/execution/DaemonThreadFactory.java | 16 +- .../util/execution/ProgramExecutor.java | 211 ++- .../shared/util/execution/ProgramResult.java | 58 +- .../util/execution/StreamsProcessOutput.java | 16 +- .../util/execution/WaitingFutureTask.java | 96 +- .../shared/util/systeminfo/CoreInfo.java | 122 +- .../util/systeminfo/CoreInfoParser.java | 106 +- .../shared/util/systeminfo/CpuInfo.java | 131 +- .../util/systeminfo/LinuxSystemInfo.java | 78 +- .../shared/util/systeminfo/MemoryInfo.java | 154 +- .../shared/protocol/HeartbeatHandlerTest.java | 77 +- .../serialization/SerializerBasedTest.java | 26 +- .../serialization/SerializerTest.java | 8 +- .../clientbound/AbortRunTest.java | 23 +- .../clientbound/ClearResultTest.java | 20 +- .../clientbound/ClientBoundPacketTest.java | 54 +- .../clientbound/GetResultTest.java | 20 +- .../clientbound/GetStatusTest.java | 20 +- .../clientbound/RequestRunReplyTest.java | 44 +- .../serverbound/AbortRunReplyTest.java | 23 +- .../serverbound/ClearResultReplyTest.java | 20 +- .../serverbound/GetResultReplyTest.java | 257 ++- .../serverbound/GetStatusReplyTest.java | 86 +- .../serverbound/RequestRunTest.java | 20 +- .../serverbound/ServerBoundPacketTest.java | 54 +- .../statemachine/StateMachineTest.java | 198 +-- .../velcom/shared/util/EitherTest.java | 162 +- .../shared/util/ExceptionHelperTest.java | 64 +- .../velcom/shared/util/FileHelperTest.java | 244 ++- .../velcom/shared/util/OSCheckTest.java | 18 +- .../aaaaaaah/velcom/shared/util/PairTest.java | 19 +- .../shared/util/StringOutputStreamTest.java | 102 +- .../velcom/shared/util/TimeoutTest.java | 82 +- .../compression/PermissionsHelperTest.java | 38 +- .../util/compression/TarHelperTest.java | 152 +- .../util/execution/ProgramExecutorTest.java | 367 ++-- .../util/systeminfo/CoreInfoParserTest.java | 157 +- .../shared/util/systeminfo/CpuInfoTest.java | 288 ++- .../util/systeminfo/LinuxSystemInfoTest.java | 57 +- .../util/systeminfo/MemoryInfoTest.java | 69 +- frontend/README.md | 5 + frontend/babel.config.js | 4 +- .../public/fonts/fontface-declaration.css | 311 ++-- frontend/public/index.html | 8 +- frontend/src/App.vue | 74 +- frontend/src/ansi-to-html.d.ts | 2 +- frontend/src/axios-config-userdata.d.ts | 18 +- frontend/src/class-components-router-hooks.ts | 10 +- .../components/cleanup/CleanupDimensions.vue | 93 +- .../comparison/ComparisonRunInfo.vue | 41 +- .../comparison/RunComparisonTable.vue | 175 +- .../ComparisonDimensionSelector.vue | 56 +- .../graphs/comparison/ComparisonGraph.vue | 117 +- .../graphs/comparison/RepoBranchSelector.vue | 58 +- .../detail/MatrixDimensionSelection.vue | 136 +- .../components/graphs/detail/RepoGraph.vue | 153 +- .../graphs/detail/TreeDimensionSelection.vue | 122 +- .../components/graphs/graph/DytailGraph.vue | 407 ++--- .../graphs/graph/EchartsDetailGraph.vue | 728 ++++---- .../helper/ExpandableDimensionSelection.vue | 30 +- .../graphs/helper/GraphDatapointDialog.vue | 109 +- .../graphs/helper/GraphSettings.vue | 51 +- .../graphs/helper/GraphTimespanControls.vue | 152 +- .../graphs/helper/OverscrollToZoom.ts | 51 +- .../graphs/helper/ShareGraphLinkDialog.vue | 71 +- .../StatusComparisonGraph.vue | 491 +++--- .../components/misc/FileSelectComponent.vue | 33 +- .../misc/InlineMinimalRepoDisplay.vue | 25 +- frontend/src/components/misc/LoginDialog.vue | 36 +- .../src/components/misc/NavigationBar.vue | 85 +- .../misc/RepoSelectionComponent.vue | 32 +- frontend/src/components/misc/Snackbar.vue | 101 +- frontend/src/components/misc/TextChip.vue | 22 +- .../src/components/misc/ThemeSelector.vue | 25 +- .../overviews/CommitOverviewBase.vue | 59 +- .../overviews/MultipleRunOverview.vue | 42 +- .../src/components/overviews/RunOverview.vue | 63 +- .../src/components/overviews/TarOverview.vue | 36 +- .../components/overviews/TarTaskOverview.vue | 43 +- .../src/components/queue/QueueOverview.vue | 174 +- .../src/components/queue/TaskRunnerOutput.vue | 89 +- .../src/components/queue/UploadTarDialog.vue | 59 +- .../src/components/queue/WorkerOverview.vue | 22 +- .../repodetail/GithubBotCommandChips.vue | 65 +- .../repodetail/GithubTokenConfiguration.vue | 32 +- .../repodetail/GraphDimensionSelector.vue | 60 +- .../components/repodetail/RepoAddDialog.vue | 36 +- .../repodetail/RepoBaseInformation.vue | 121 +- .../repodetail/RepoUpdateDialog.vue | 108 +- .../src/components/rundetail/CommitDetail.vue | 81 +- .../rundetail/CommitNavigationButton.vue | 26 +- .../rundetail/MeasurementValueDisplay.vue | 18 +- .../rundetail/MeasurementsDisplay.vue | 272 ++- .../src/components/rundetail/RunDetail.vue | 60 +- frontend/src/components/rundetail/RunInfo.vue | 64 +- .../src/components/rundetail/RunTimeline.vue | 48 +- .../runs/CommitBenchmarkActions.vue | 140 +- .../components/runs/RunSignificanceChips.vue | 139 +- .../components/search/SearchResultBranch.vue | 32 +- .../components/search/SearchResultCommit.vue | 42 +- .../components/search/SearchResultList.vue | 64 +- .../src/components/search/SearchResultRun.vue | 49 +- frontend/src/css/AnsiTheme.css | 128 +- frontend/src/diegraphs-extra.d.ts | 2 +- frontend/src/main.ts | 129 +- frontend/src/plugins/vuetify.ts | 74 +- frontend/src/router/index.ts | 263 ++- frontend/src/scss/variables.scss | 14 +- frontend/src/shims-tsx.d.ts | 4 +- frontend/src/shims-vue.d.ts | 6 +- frontend/src/shims-vuetify.d.ts | 6 +- frontend/src/store/index.ts | 68 +- frontend/src/store/modules/cleanupStore.ts | 30 +- frontend/src/store/modules/colorStore.ts | 110 +- .../modules/commitDetailComparisonStore.ts | 76 +- .../src/store/modules/comparisonGraphStore.ts | 194 +-- .../src/store/modules/detailGraphStore.ts | 298 ++-- frontend/src/store/modules/newsStore.ts | 62 +- frontend/src/store/modules/queueStore.ts | 146 +- frontend/src/store/modules/repoStore.ts | 139 +- frontend/src/store/modules/runSearchStore.ts | 32 +- .../store/modules/statusComparisonStore.ts | 134 +- frontend/src/store/modules/userStore.ts | 48 +- frontend/src/store/persistence.ts | 144 +- frontend/src/store/types.ts | 780 ++++----- frontend/src/util/Arrays.ts | 8 +- frontend/src/util/Clipboards.ts | 28 +- frontend/src/util/ColorUtils.ts | 84 +- frontend/src/util/DayEquidistantUtils.ts | 49 +- frontend/src/util/Debouncer.ts | 16 +- frontend/src/util/Errors.ts | 16 +- frontend/src/util/FlavorTypes.ts | 4 +- frontend/src/util/GraphVariantSelection.ts | 34 +- frontend/src/util/LinkUtils.ts | 89 +- frontend/src/util/Measurements.ts | 13 +- frontend/src/util/Serialisation.ts | 68 +- frontend/src/util/Snackbar.ts | 8 +- frontend/src/util/Texts.ts | 34 +- frontend/src/util/Times.ts | 141 +- frontend/src/util/json/CleanupJsonHelper.ts | 8 +- .../util/json/CommitComparisonJsonHelper.ts | 80 +- frontend/src/util/json/GraphJsonHelper.ts | 56 +- frontend/src/util/json/NewJsonHelper.ts | 17 +- frontend/src/util/json/QueueJsonHelper.ts | 31 +- frontend/src/util/json/RepoJsonHelper.ts | 21 +- frontend/src/util/json/RunSearchJsonHelper.ts | 21 +- .../util/json/StatusComparisonJsonHelper.ts | 12 +- frontend/src/views/About.vue | 199 +-- frontend/src/views/Home.vue | 62 +- frontend/src/views/NotFound404.vue | 14 +- frontend/src/views/Queue.vue | 80 +- frontend/src/views/RepoComparison.vue | 34 +- frontend/src/views/RepoDetailFrame.vue | 74 +- frontend/src/views/RepoGraphView.vue | 97 +- frontend/src/views/RunCommitDetailView.vue | 143 +- frontend/src/views/RunComparison.vue | 76 +- frontend/src/views/Search.vue | 125 +- frontend/src/views/Settings.vue | 12 +- frontend/src/views/StatusComparison.vue | 163 +- frontend/src/views/TaskDetailView.vue | 146 +- frontend/src/views/TimelineComparison.vue | 191 +- frontend/src/vue-class-components.d.ts | 18 +- frontend/src/vue-echarts.d.ts | 2 +- frontend/src/vue-snackbar.d.ts | 6 +- frontend/tsconfig.json | 28 +- frontend/vue.config.js | 21 +- 424 files changed, 29108 insertions(+), 29566 deletions(-) diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/GlobalConfig.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/GlobalConfig.java index 6b33e8db9..c9e42ccb5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/GlobalConfig.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/GlobalConfig.java @@ -9,234 +9,231 @@ import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; -/** - * The main configuration file for the server. - */ +/** The main configuration file for the server. */ public class GlobalConfig extends Configuration { - ///////////// - // General // - ///////////// - - @NotEmpty - private String webAdminToken; - @NotEmpty - private String benchmarkRepoRemoteUrl; - @NotEmpty - private String frontendUrl; - private long pollInterval = 10 * 60; - private long vacuumInterval = 25 * 60 * 60; - - ///////////////// - // Directories // - ///////////////// - - private Path dataDir = Path.of("data"); - private Path cacheDir = Path.of("cache"); - private Path tmpDir = Path.of("tmp"); - - //////////// - // Runner // - //////////// - - @NotEmpty - private String runnerToken; - @Min(1) - @Max(65535) - private int runnerPort = 3546; - @Min(1) - private long disconnectedRunnerGracePeriodSeconds = 600; - - ///////////////////////// - // Significant commits // - ///////////////////////// - - private double significanceRelativeThreshold = 0.05; - private double significanceStddevThreshold = 2; - @Min(2) - private int significanceMinStddevAmount = 25; - - public GlobalConfig() { - RunnerAwareServerFactory.getInstance().setConfig(this); - } - - @Override - public ServerFactory getServerFactory() { - if (RunnerAwareServerFactory.getInstance().lacksFactory()) { - RunnerAwareServerFactory.getInstance().setServerFactory(super.getServerFactory()); - } - return RunnerAwareServerFactory.getInstance(); - } - - ///////////// - // General // - ///////////// - - /** - * @return the token used to authorize as a web administrator - */ - public String getWebAdminToken() { - return webAdminToken; - } - - public void setWebAdminToken(String webAdminToken) { - this.webAdminToken = webAdminToken; - } - - /** - * @return the remote url where the benchmark repo remote url can be cloned from - */ - public String getBenchmarkRepoRemoteUrl() { - return benchmarkRepoRemoteUrl; - } - - public void setBenchmarkRepoRemoteUrl(String benchmarkRepoRemoteUrl) { - this.benchmarkRepoRemoteUrl = benchmarkRepoRemoteUrl; - } - - /** - * @return the base url the frontend can be reached under, including a trailing slash - */ - public String getFrontendUrl() { - if (frontendUrl.endsWith("/")) { - return frontendUrl; - } else { - return frontendUrl + "/"; - } - } - - public void setFrontendUrl(String frontendUrl) { - this.frontendUrl = frontendUrl; - } - - /** - * @return the interval between listener updates - */ - public Duration getPollInterval() { - return Duration.ofSeconds(pollInterval); - } - - public void setPollInterval(long pollInterval) { - this.pollInterval = pollInterval; - } - - public Duration getVacuumInterval() { - return Duration.ofSeconds(vacuumInterval); - } - - public void setVacuumInterval(long vacuumInterval) { - this.vacuumInterval = vacuumInterval; - } - - ///////////////// - // Directories // - ///////////////// - - @Nullable - public Path getDataDir() { - return dataDir; - } - - public void setDataDir(@Nullable Path dataDir) { - this.dataDir = dataDir; - } - - @Nullable - public Path getCacheDir() { - return cacheDir; - } - - public void setCacheDir(@Nullable Path cacheDir) { - this.cacheDir = cacheDir; - } - - @Nullable - public Path getTmpDir() { - return tmpDir; - } - - public void setTmpDir(@Nullable Path tmpDir) { - this.tmpDir = tmpDir; - } - - //////////// - // Runner // - //////////// - - /** - * @return the token runners need to provide as authentication - */ - public String getRunnerToken() { - return runnerToken; - } - - public void setRunnerToken(String runnerToken) { - this.runnerToken = runnerToken; - } - - /** - * @return the port that the dispatcher is listening on - */ - public int getRunnerPort() { - return runnerPort; - } - - public void setRunnerPort(int runnerPort) { - this.runnerPort = runnerPort; - } - - /** - * @return the duration after which disconnected runners are given up on (removed and commit - * rescheduled) - */ - public Duration getDisconnectedRunnerGracePeriod() { - return Duration.ofSeconds(disconnectedRunnerGracePeriodSeconds); - } - - public void setDisconnectedRunnerGracePeriodSeconds(long disconnectedRunnerGracePeriodSeconds) { - this.disconnectedRunnerGracePeriodSeconds = disconnectedRunnerGracePeriodSeconds; - } - - ///////////////////////// - // Significant commits // - ///////////////////////// - - /** - * Explained in more detail in the example config. - * - * @return the relative threshold - */ - public double getSignificanceRelativeThreshold() { - return significanceRelativeThreshold; - } - - public void setSignificanceRelativeThreshold(double significanceRelativeThreshold) { - this.significanceRelativeThreshold = significanceRelativeThreshold; - } - - /** - * Explained in more detail in the example config. - * - * @return the stddev threshold - */ - public double getSignificanceStddevThreshold() { - return significanceStddevThreshold; - } - - public void setSignificanceStddevThreshold(double significanceStddevThreshold) { - this.significanceStddevThreshold = significanceStddevThreshold; - } - - /** - * Explained in more detail in the example config. - * - * @return the minimum amount of values for stddev calculations to apply - */ - public int getSignificanceMinStddevAmount() { - return significanceMinStddevAmount; - } - - public void setSignificanceMinStddevAmount(int significanceMinStddevAmount) { - this.significanceMinStddevAmount = significanceMinStddevAmount; - } + ///////////// + // General // + ///////////// + + @NotEmpty private String webAdminToken; + @NotEmpty private String benchmarkRepoRemoteUrl; + @NotEmpty private String frontendUrl; + private long pollInterval = 10 * 60; + private long vacuumInterval = 25 * 60 * 60; + + ///////////////// + // Directories // + ///////////////// + + private Path dataDir = Path.of("data"); + private Path cacheDir = Path.of("cache"); + private Path tmpDir = Path.of("tmp"); + + //////////// + // Runner // + //////////// + + @NotEmpty private String runnerToken; + + @Min(1) + @Max(65535) + private int runnerPort = 3546; + + @Min(1) + private long disconnectedRunnerGracePeriodSeconds = 600; + + ///////////////////////// + // Significant commits // + ///////////////////////// + + private double significanceRelativeThreshold = 0.05; + private double significanceStddevThreshold = 2; + + @Min(2) + private int significanceMinStddevAmount = 25; + + public GlobalConfig() { + RunnerAwareServerFactory.getInstance().setConfig(this); + } + + @Override + public ServerFactory getServerFactory() { + if (RunnerAwareServerFactory.getInstance().lacksFactory()) { + RunnerAwareServerFactory.getInstance().setServerFactory(super.getServerFactory()); + } + return RunnerAwareServerFactory.getInstance(); + } + + ///////////// + // General // + ///////////// + + /** + * @return the token used to authorize as a web administrator + */ + public String getWebAdminToken() { + return webAdminToken; + } + + public void setWebAdminToken(String webAdminToken) { + this.webAdminToken = webAdminToken; + } + + /** + * @return the remote url where the benchmark repo remote url can be cloned from + */ + public String getBenchmarkRepoRemoteUrl() { + return benchmarkRepoRemoteUrl; + } + + public void setBenchmarkRepoRemoteUrl(String benchmarkRepoRemoteUrl) { + this.benchmarkRepoRemoteUrl = benchmarkRepoRemoteUrl; + } + + /** + * @return the base url the frontend can be reached under, including a trailing slash + */ + public String getFrontendUrl() { + if (frontendUrl.endsWith("/")) { + return frontendUrl; + } else { + return frontendUrl + "/"; + } + } + + public void setFrontendUrl(String frontendUrl) { + this.frontendUrl = frontendUrl; + } + + /** + * @return the interval between listener updates + */ + public Duration getPollInterval() { + return Duration.ofSeconds(pollInterval); + } + + public void setPollInterval(long pollInterval) { + this.pollInterval = pollInterval; + } + + public Duration getVacuumInterval() { + return Duration.ofSeconds(vacuumInterval); + } + + public void setVacuumInterval(long vacuumInterval) { + this.vacuumInterval = vacuumInterval; + } + + ///////////////// + // Directories // + ///////////////// + + @Nullable + public Path getDataDir() { + return dataDir; + } + + public void setDataDir(@Nullable Path dataDir) { + this.dataDir = dataDir; + } + + @Nullable + public Path getCacheDir() { + return cacheDir; + } + + public void setCacheDir(@Nullable Path cacheDir) { + this.cacheDir = cacheDir; + } + + @Nullable + public Path getTmpDir() { + return tmpDir; + } + + public void setTmpDir(@Nullable Path tmpDir) { + this.tmpDir = tmpDir; + } + + //////////// + // Runner // + //////////// + + /** + * @return the token runners need to provide as authentication + */ + public String getRunnerToken() { + return runnerToken; + } + + public void setRunnerToken(String runnerToken) { + this.runnerToken = runnerToken; + } + + /** + * @return the port that the dispatcher is listening on + */ + public int getRunnerPort() { + return runnerPort; + } + + public void setRunnerPort(int runnerPort) { + this.runnerPort = runnerPort; + } + + /** + * @return the duration after which disconnected runners are given up on (removed and commit + * rescheduled) + */ + public Duration getDisconnectedRunnerGracePeriod() { + return Duration.ofSeconds(disconnectedRunnerGracePeriodSeconds); + } + + public void setDisconnectedRunnerGracePeriodSeconds(long disconnectedRunnerGracePeriodSeconds) { + this.disconnectedRunnerGracePeriodSeconds = disconnectedRunnerGracePeriodSeconds; + } + + ///////////////////////// + // Significant commits // + ///////////////////////// + + /** + * Explained in more detail in the example config. + * + * @return the relative threshold + */ + public double getSignificanceRelativeThreshold() { + return significanceRelativeThreshold; + } + + public void setSignificanceRelativeThreshold(double significanceRelativeThreshold) { + this.significanceRelativeThreshold = significanceRelativeThreshold; + } + + /** + * Explained in more detail in the example config. + * + * @return the stddev threshold + */ + public double getSignificanceStddevThreshold() { + return significanceStddevThreshold; + } + + public void setSignificanceStddevThreshold(double significanceStddevThreshold) { + this.significanceStddevThreshold = significanceStddevThreshold; + } + + /** + * Explained in more detail in the example config. + * + * @return the minimum amount of values for stddev calculations to apply + */ + public int getSignificanceMinStddevAmount() { + return significanceMinStddevAmount; + } + + public void setSignificanceMinStddevAmount(int significanceMinStddevAmount) { + this.significanceMinStddevAmount = significanceMinStddevAmount; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/KnownHostsIgnoringSshdFactory.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/KnownHostsIgnoringSshdFactory.java index beb29aabc..48e5d856e 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/KnownHostsIgnoringSshdFactory.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/KnownHostsIgnoringSshdFactory.java @@ -11,29 +11,31 @@ import org.eclipse.jgit.transport.sshd.ServerKeyDatabase; import org.eclipse.jgit.transport.sshd.SshdSessionFactory; -/** - * A {@link SshdSessionFactory} that ignores server keys. - */ +/** A {@link SshdSessionFactory} that ignores server keys. */ public class KnownHostsIgnoringSshdFactory extends SshdSessionFactory { - public KnownHostsIgnoringSshdFactory(KeyCache keyCache, ProxyDataFactory proxies) { - super(keyCache, proxies); - } + public KnownHostsIgnoringSshdFactory(KeyCache keyCache, ProxyDataFactory proxies) { + super(keyCache, proxies); + } - @Override - protected ServerKeyDatabase getServerKeyDatabase(File homeDir, File sshDir) { - return new ServerKeyDatabase() { - @Override - public List lookup(String connectAddress, InetSocketAddress remoteAddress, - Configuration config) { - return Collections.emptyList(); - } + @Override + protected ServerKeyDatabase getServerKeyDatabase(File homeDir, File sshDir) { + return new ServerKeyDatabase() { + @Override + public List lookup( + String connectAddress, InetSocketAddress remoteAddress, Configuration config) { + return Collections.emptyList(); + } - @Override - public boolean accept(String connectAddress, InetSocketAddress remoteAddress, - PublicKey serverKey, Configuration config, CredentialsProvider provider) { - return true; - } - }; - } + @Override + public boolean accept( + String connectAddress, + InetSocketAddress remoteAddress, + PublicKey serverKey, + Configuration config, + CredentialsProvider provider) { + return true; + } + }; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/RunnerAwareServerFactory.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/RunnerAwareServerFactory.java index fea183e35..af7c7da93 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/RunnerAwareServerFactory.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/RunnerAwareServerFactory.java @@ -24,141 +24,139 @@ */ public class RunnerAwareServerFactory implements ServerFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(RunnerAwareServerFactory.class); - - private static final RunnerAwareServerFactory instance = new RunnerAwareServerFactory(); - - private ServerFactory underlying; - private GlobalConfig config; - private Dispatcher dispatcher; - private BenchRepo benchRepo; - private final Serializer serializer; - - private RunnerAwareServerFactory() { - this.serializer = new Serializer(); - } - - @Override - public Server build(Environment environment) { - ensureIsInitialized(); - - Server server = underlying.build(environment); - - HttpConnectorFactory connectorFactory = new HttpConnectorFactory(); - connectorFactory.setPort(config.getRunnerPort()); - Connector connector = connectorFactory - .build(server, environment.metrics(), "Runner", new ExecutorThreadPool(32)); - server.addConnector( - connector - ); - Map handlerMap = new HashMap<>(); - - for (Connector serverConnector : server.getConnectors()) { - handlerMap.put(serverConnector, server.getHandler()); - } - - MutableServletContextHandler handler = new MutableServletContextHandler(); - handler.getServletContext().addServlet( - "Runner Websocket servlet", - new ServerMasterWebsocketServlet(dispatcher, serializer, config.getRunnerToken(), benchRepo) - ) - .addMapping("/runner-connector"); - handlerMap.put(connector, handler); - - server.setHandler(new RoutingHandler(handlerMap)); - - LOGGER.debug("Registered the websocket servlet"); - - return server; - } - - @Override - public void configure(Environment environment) { - underlying.configure(environment); - } - - - /** - * Sets the config to use. - * - * @param config the config to use - * @throws IllegalStateException if a config has already been set - */ - public void setConfig(GlobalConfig config) { - if (this.config != null) { - throw new IllegalStateException("I already have a config!"); - } - this.config = config; - } - - /** - * Sets the server factory to delegate to. - * - * @param underlying the underlying server factory - * @throws IllegalStateException if a server factory has already been set - */ - public void setServerFactory(ServerFactory underlying) { - if (this.underlying != null) { - throw new IllegalStateException("I already have a server factory!"); - } - - this.underlying = underlying; - } - - /** - * Sets the dispatcher to use. - * - * @param dispatcher the dispatcher - * @throws IllegalStateException if a dispatcher has already been set - */ - public void setDispatcher(Dispatcher dispatcher) { - if (this.dispatcher != null) { - throw new IllegalStateException("I already have a dispatcher!"); - } - this.dispatcher = dispatcher; - } - - /** - * Sets the bench reo to use. - * - * @param benchRepo the bench repo - * @throws IllegalStateException if a bench repo has already been set - */ - public void setBenchRepo(BenchRepo benchRepo) { - if (this.benchRepo != null) { - throw new IllegalStateException("I already have a bench repo!"); - } - this.benchRepo = benchRepo; - } - - /** - * Returns whether the factory lacks an underlying server factory. - * - * @return true if no underlying server factory is available - */ - public boolean lacksFactory() { - return underlying == null; - } - - private void ensureIsInitialized() { - if (config == null) { - throw new IllegalStateException("Config not initialized"); - } - if (underlying == null) { - throw new IllegalStateException("Server factory not initialized"); - } - if (dispatcher == null) { - throw new IllegalStateException("Dispatcher not initialized"); - } - } - - - /** - * Returns the instance of the {@link RunnerAwareServerFactory}. - * - * @return the instance - */ - public static RunnerAwareServerFactory getInstance() { - return instance; - } + private static final Logger LOGGER = LoggerFactory.getLogger(RunnerAwareServerFactory.class); + + private static final RunnerAwareServerFactory instance = new RunnerAwareServerFactory(); + + private ServerFactory underlying; + private GlobalConfig config; + private Dispatcher dispatcher; + private BenchRepo benchRepo; + private final Serializer serializer; + + private RunnerAwareServerFactory() { + this.serializer = new Serializer(); + } + + @Override + public Server build(Environment environment) { + ensureIsInitialized(); + + Server server = underlying.build(environment); + + HttpConnectorFactory connectorFactory = new HttpConnectorFactory(); + connectorFactory.setPort(config.getRunnerPort()); + Connector connector = + connectorFactory.build(server, environment.metrics(), "Runner", new ExecutorThreadPool(32)); + server.addConnector(connector); + Map handlerMap = new HashMap<>(); + + for (Connector serverConnector : server.getConnectors()) { + handlerMap.put(serverConnector, server.getHandler()); + } + + MutableServletContextHandler handler = new MutableServletContextHandler(); + handler + .getServletContext() + .addServlet( + "Runner Websocket servlet", + new ServerMasterWebsocketServlet( + dispatcher, serializer, config.getRunnerToken(), benchRepo)) + .addMapping("/runner-connector"); + handlerMap.put(connector, handler); + + server.setHandler(new RoutingHandler(handlerMap)); + + LOGGER.debug("Registered the websocket servlet"); + + return server; + } + + @Override + public void configure(Environment environment) { + underlying.configure(environment); + } + + /** + * Sets the config to use. + * + * @param config the config to use + * @throws IllegalStateException if a config has already been set + */ + public void setConfig(GlobalConfig config) { + if (this.config != null) { + throw new IllegalStateException("I already have a config!"); + } + this.config = config; + } + + /** + * Sets the server factory to delegate to. + * + * @param underlying the underlying server factory + * @throws IllegalStateException if a server factory has already been set + */ + public void setServerFactory(ServerFactory underlying) { + if (this.underlying != null) { + throw new IllegalStateException("I already have a server factory!"); + } + + this.underlying = underlying; + } + + /** + * Sets the dispatcher to use. + * + * @param dispatcher the dispatcher + * @throws IllegalStateException if a dispatcher has already been set + */ + public void setDispatcher(Dispatcher dispatcher) { + if (this.dispatcher != null) { + throw new IllegalStateException("I already have a dispatcher!"); + } + this.dispatcher = dispatcher; + } + + /** + * Sets the bench reo to use. + * + * @param benchRepo the bench repo + * @throws IllegalStateException if a bench repo has already been set + */ + public void setBenchRepo(BenchRepo benchRepo) { + if (this.benchRepo != null) { + throw new IllegalStateException("I already have a bench repo!"); + } + this.benchRepo = benchRepo; + } + + /** + * Returns whether the factory lacks an underlying server factory. + * + * @return true if no underlying server factory is available + */ + public boolean lacksFactory() { + return underlying == null; + } + + private void ensureIsInitialized() { + if (config == null) { + throw new IllegalStateException("Config not initialized"); + } + if (underlying == null) { + throw new IllegalStateException("Server factory not initialized"); + } + if (dispatcher == null) { + throw new IllegalStateException("Dispatcher not initialized"); + } + } + + /** + * Returns the instance of the {@link RunnerAwareServerFactory}. + * + * @return the instance + */ + public static RunnerAwareServerFactory getInstance() { + return instance; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/ServerMain.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/ServerMain.java index 440861be8..52a27ec74 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/ServerMain.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/ServerMain.java @@ -85,267 +85,287 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * The backend's main class. Contains the core initialisation routines for the web server. - */ +/** The backend's main class. Contains the core initialisation routines for the web server. */ public class ServerMain extends Application { - private static final Logger LOGGER = LoggerFactory.getLogger(ServerMain.class); - - /** - * The backend's main class's main method. Starts the web server. - * - * @param args the command line arguments - * @throws Exception if the web server can not be started - */ - public static void main(String[] args) throws Exception { - System.out.println("Welcome to VelCom!"); - System.out.printf("Version: %s (backend)%n", GitProperties.getVersion()); - System.out.printf("Build time: %s%n", GitProperties.getBuildTime()); - System.out.printf("Commit hash: %s%n", GitProperties.getHash()); - System.out.println(); - - new ServerMain().run(args); - } - - @Override - public void initialize(Bootstrap bootstrap) { - bootstrap.addBundle(new MultiPartBundle()); - } - - @Override - public void run(GlobalConfig configuration, Environment environment) throws Exception { - configureMetrics(environment); - configureDummyHealthCheck(environment); - - // Storage layer - ManagedDirs managedDirs = new ManagedDirs( - configuration.getDataDir(), - configuration.getCacheDir(), - configuration.getTmpDir() - ); - managedDirs.createAndCleanDirs(); - - DatabaseStorage databaseStorage = new DatabaseStorage(managedDirs.getJdbcUrl()); - RepoStorage repoStorage = new RepoStorage(managedDirs.getReposDir()); - TarFileStorage tarFileStorage = new TarFileStorage(managedDirs.getTarsDir()); - - // Caches - AvailableDimensionsCache availableDimensionsCache = new AvailableDimensionsCache(); - LatestRunCache latestRunCache = new LatestRunCache(); - RunCache runCache = new RunCache(); - - // Access layer - TaskWriteAccess taskAccess = new TaskWriteAccess(databaseStorage, tarFileStorage); - CommitReadAccess commitAccess = new CommitReadAccess(databaseStorage); - DimensionWriteAccess dimensionAccess = new DimensionWriteAccess(databaseStorage, - availableDimensionsCache, runCache); - RepoWriteAccess repoAccess = new RepoWriteAccess(databaseStorage, availableDimensionsCache, - runCache, latestRunCache); - ArchiveReadAccess archiveAccess = new ArchiveReadAccess( - managedDirs.getArchivesDir(), - repoStorage, - tarFileStorage, - configuration.getBenchmarkRepoRemoteUrl() - ); - BenchmarkWriteAccess benchmarkAccess = new BenchmarkWriteAccess(databaseStorage, - availableDimensionsCache, latestRunCache); - - taskAccess.resetAllTaskStatuses(); - taskAccess.cleanUpTarFiles(); - - // Data layer - Queue queue = new Queue(taskAccess, archiveAccess, benchmarkAccess); - BenchRepo benchRepo = new BenchRepo(archiveAccess); - SignificanceFactors significanceFactors = new SignificanceFactors( - configuration.getSignificanceRelativeThreshold(), - configuration.getSignificanceStddevThreshold(), - configuration.getSignificanceMinStddevAmount() - ); - RunComparator runComparator = new RunComparator(significanceFactors); - SignificanceDetector significanceDetector = new SignificanceDetector(significanceFactors, - runComparator); - SignificantRunsCollector significantRunsCollector = new SignificantRunsCollector( - significanceFactors, benchmarkAccess, commitAccess, dimensionAccess, runCache, latestRunCache, - significanceDetector); - - // Listener - Listener listener = new Listener( - databaseStorage, - repoStorage, - benchmarkAccess, - commitAccess, - dimensionAccess, - repoAccess, - benchRepo, - significanceDetector, - queue, - configuration.getPollInterval(), - configuration.getVacuumInterval(), - configuration.getFrontendUrl() - ); - - // Dispatcher - Dispatcher dispatcher = new Dispatcher( - queue, - configuration.getDisconnectedRunnerGracePeriod() - ); - RunnerAwareServerFactory.getInstance().setDispatcher(dispatcher); - RunnerAwareServerFactory.getInstance().setBenchRepo(benchRepo); - - // API - configureSerialization(environment); - configureExceptionMappers(environment); - configureAuthentication(environment, configuration.getWebAdminToken()); - configureCors(environment); - - // Endpoints - Stream.of( - new AllDimensionsEndpoint(dimensionAccess), - new AllReposEndpoint(dimensionAccess, repoAccess, availableDimensionsCache), - new CommitEndpoint(benchmarkAccess, commitAccess, runCache), - new CompareEndpoint(benchmarkAccess, commitAccess, dimensionAccess, runCache, latestRunCache, - runComparator, significanceDetector, significanceFactors), - new DebugEndpoint(benchmarkAccess, dispatcher), - new DimensionsEndpoint(dimensionAccess), - new GraphComparisonEndpoint(benchmarkAccess, commitAccess, dimensionAccess), - new GraphDetailEndpoint(commitAccess, benchmarkAccess, dimensionAccess, repoAccess, runCache, - latestRunCache), - new GraphStatusComparisonEndpoint(benchmarkAccess, commitAccess, dimensionAccess, repoAccess, - significanceFactors), - new ListenerEndpoint(listener), - new QueueEndpoint(commitAccess, repoAccess, queue, dispatcher), - new RecentRunsEndpoint(benchmarkAccess, commitAccess, dimensionAccess, runCache, - significantRunsCollector), - new RepoEndpoint(dimensionAccess, repoAccess, availableDimensionsCache, - listener), - new RunEndpoint(benchmarkAccess, commitAccess, dimensionAccess, runCache, latestRunCache, - runComparator, significanceFactors, significantRunsCollector), - new SearchEndpoint(benchmarkAccess, commitAccess, repoAccess), - new TestTokenEndpoint() - ).forEach(endpoint -> environment.jersey().register(endpoint)); - } - - private void configureMetrics(Environment environment) { - PrometheusMeterRegistry registry = new PrometheusMeterRegistry( - PrometheusConfig.DEFAULT - ); - - registry.config().commonTags("application", "Velcom"); - - Metrics.globalRegistry.add(registry); - - Metrics.globalRegistry.config().meterFilter(new MeterFilter() { - @Override - public MeterFilterReply accept(Id id) { - return MeterFilterReply.NEUTRAL; - } - - @Override - public Id map(Id id) { - final String cacheTag = id.getTag("cache"); - if (cacheTag == null) { - return id; - } - - // meter is a cache meter => prepend cacheTag to name - return new Id( - cacheTag + "_" + id.getName(), - Tags.of(id.getTags()), - id.getBaseUnit(), - id.getDescription(), - id.getType() - ); - } - }); - - try { - Class.forName("org.aspectj.weaver.WeaverMessages"); - LOGGER.info("AspectJ weaver agent detected, providing detailed timing metrics"); - Gauge.builder("aspectj.weaver.enabled", () -> 1).register(registry); - } catch (ClassNotFoundException e) { - LOGGER.warn("AspectJ weaver agent NOT FOUND! Many metrics will NOT be available"); - Gauge.builder("aspectj.weaver.enabled", () -> 0).register(registry); - } - - new ClassLoaderMetrics().bindTo(Metrics.globalRegistry); - new JvmCompilationMetrics().bindTo(Metrics.globalRegistry); - new JvmGcMetrics().bindTo(Metrics.globalRegistry); - new JvmHeapPressureMetrics().bindTo(Metrics.globalRegistry); - new JvmMemoryMetrics().bindTo(Metrics.globalRegistry); - new JvmThreadMetrics().bindTo(Metrics.globalRegistry); - new ProcessorMetrics().bindTo(Metrics.globalRegistry); - new UptimeMetrics().bindTo(Metrics.globalRegistry); - - environment.admin() - .addServlet("prometheusMetrics", new HttpServlet() { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) - throws IOException { - resp.setStatus(HttpServletResponse.SC_OK); - resp.getWriter().write(registry.scrape()); - } - }) - .addMapping("/prometheusMetrics"); - } - - /** - * Since we've never used and don't plan on using health checks any time soon, this function - * disables the big scary warning dropwizard shows on startup. - */ - private void configureDummyHealthCheck(Environment environment) { - environment.healthChecks().register("dummy", new HealthCheck() { - @Override - protected Result check() { - return Result.healthy(); - } - }); - } - - private void configureSerialization(Environment environment) { - // This mapper should be configured the same as the one in SerializingTest.java - environment.getObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) - .setSerializationInclusion(Include.NON_NULL); - } - - private void configureExceptionMappers(Environment environment) { - environment.jersey().register(new InvalidQueryParamsExceptionMapper()); - environment.jersey().register(new NoSuchCommitExceptionMapper()); - environment.jersey().register(new NoSuchDimensionExceptionMapper()); - environment.jersey().register(new NoSuchRepoExceptionMapper()); - environment.jersey().register(new NoSuchRunExceptionMapper()); - environment.jersey().register(new NoSuchTaskExceptionMapper()); - environment.jersey().register(new TaskAlreadyExistsExceptionMapper()); - environment.jersey().register(new ArgumentParseExceptionMapper()); - } - - private void configureAuthentication(Environment environment, String adminToken) { - environment.jersey().register( - new AuthDynamicFeature( - new BasicCredentialAuthFilter.Builder() - .setAuthenticator(new AdminAuthenticator(adminToken)) - .buildAuthFilter() - ) - ); - environment.jersey().register(new AuthValueFactoryProvider.Binder<>(Admin.class)); - } - - private void configureCors(Environment environment) { - var filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class); - filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); - filter.setInitParameter( - CrossOriginFilter.ALLOWED_METHODS_PARAM, - "GET,PUT,POST,DELETE,OPTIONS,PATCH" - ); - filter.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); - filter.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*"); - filter.setInitParameter( - "allowedHeaders", - "Content-Type,Authorization,X-Requested-With,Content-Length,Accept-Encoding,Origin" - ); - filter.setInitParameter("allowCredentials", "true"); - } - - + private static final Logger LOGGER = LoggerFactory.getLogger(ServerMain.class); + + /** + * The backend's main class's main method. Starts the web server. + * + * @param args the command line arguments + * @throws Exception if the web server can not be started + */ + public static void main(String[] args) throws Exception { + System.out.println("Welcome to VelCom!"); + System.out.printf("Version: %s (backend)%n", GitProperties.getVersion()); + System.out.printf("Build time: %s%n", GitProperties.getBuildTime()); + System.out.printf("Commit hash: %s%n", GitProperties.getHash()); + System.out.println(); + + new ServerMain().run(args); + } + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new MultiPartBundle()); + } + + @Override + public void run(GlobalConfig configuration, Environment environment) throws Exception { + configureMetrics(environment); + configureDummyHealthCheck(environment); + + // Storage layer + ManagedDirs managedDirs = + new ManagedDirs( + configuration.getDataDir(), configuration.getCacheDir(), configuration.getTmpDir()); + managedDirs.createAndCleanDirs(); + + DatabaseStorage databaseStorage = new DatabaseStorage(managedDirs.getJdbcUrl()); + RepoStorage repoStorage = new RepoStorage(managedDirs.getReposDir()); + TarFileStorage tarFileStorage = new TarFileStorage(managedDirs.getTarsDir()); + + // Caches + AvailableDimensionsCache availableDimensionsCache = new AvailableDimensionsCache(); + LatestRunCache latestRunCache = new LatestRunCache(); + RunCache runCache = new RunCache(); + + // Access layer + TaskWriteAccess taskAccess = new TaskWriteAccess(databaseStorage, tarFileStorage); + CommitReadAccess commitAccess = new CommitReadAccess(databaseStorage); + DimensionWriteAccess dimensionAccess = + new DimensionWriteAccess(databaseStorage, availableDimensionsCache, runCache); + RepoWriteAccess repoAccess = + new RepoWriteAccess(databaseStorage, availableDimensionsCache, runCache, latestRunCache); + ArchiveReadAccess archiveAccess = + new ArchiveReadAccess( + managedDirs.getArchivesDir(), + repoStorage, + tarFileStorage, + configuration.getBenchmarkRepoRemoteUrl()); + BenchmarkWriteAccess benchmarkAccess = + new BenchmarkWriteAccess(databaseStorage, availableDimensionsCache, latestRunCache); + + taskAccess.resetAllTaskStatuses(); + taskAccess.cleanUpTarFiles(); + + // Data layer + Queue queue = new Queue(taskAccess, archiveAccess, benchmarkAccess); + BenchRepo benchRepo = new BenchRepo(archiveAccess); + SignificanceFactors significanceFactors = + new SignificanceFactors( + configuration.getSignificanceRelativeThreshold(), + configuration.getSignificanceStddevThreshold(), + configuration.getSignificanceMinStddevAmount()); + RunComparator runComparator = new RunComparator(significanceFactors); + SignificanceDetector significanceDetector = + new SignificanceDetector(significanceFactors, runComparator); + SignificantRunsCollector significantRunsCollector = + new SignificantRunsCollector( + significanceFactors, + benchmarkAccess, + commitAccess, + dimensionAccess, + runCache, + latestRunCache, + significanceDetector); + + // Listener + Listener listener = + new Listener( + databaseStorage, + repoStorage, + benchmarkAccess, + commitAccess, + dimensionAccess, + repoAccess, + benchRepo, + significanceDetector, + queue, + configuration.getPollInterval(), + configuration.getVacuumInterval(), + configuration.getFrontendUrl()); + + // Dispatcher + Dispatcher dispatcher = new Dispatcher(queue, configuration.getDisconnectedRunnerGracePeriod()); + RunnerAwareServerFactory.getInstance().setDispatcher(dispatcher); + RunnerAwareServerFactory.getInstance().setBenchRepo(benchRepo); + + // API + configureSerialization(environment); + configureExceptionMappers(environment); + configureAuthentication(environment, configuration.getWebAdminToken()); + configureCors(environment); + + // Endpoints + Stream.of( + new AllDimensionsEndpoint(dimensionAccess), + new AllReposEndpoint(dimensionAccess, repoAccess, availableDimensionsCache), + new CommitEndpoint(benchmarkAccess, commitAccess, runCache), + new CompareEndpoint( + benchmarkAccess, + commitAccess, + dimensionAccess, + runCache, + latestRunCache, + runComparator, + significanceDetector, + significanceFactors), + new DebugEndpoint(benchmarkAccess, dispatcher), + new DimensionsEndpoint(dimensionAccess), + new GraphComparisonEndpoint(benchmarkAccess, commitAccess, dimensionAccess), + new GraphDetailEndpoint( + commitAccess, + benchmarkAccess, + dimensionAccess, + repoAccess, + runCache, + latestRunCache), + new GraphStatusComparisonEndpoint( + benchmarkAccess, commitAccess, dimensionAccess, repoAccess, significanceFactors), + new ListenerEndpoint(listener), + new QueueEndpoint(commitAccess, repoAccess, queue, dispatcher), + new RecentRunsEndpoint( + benchmarkAccess, commitAccess, dimensionAccess, runCache, significantRunsCollector), + new RepoEndpoint(dimensionAccess, repoAccess, availableDimensionsCache, listener), + new RunEndpoint( + benchmarkAccess, + commitAccess, + dimensionAccess, + runCache, + latestRunCache, + runComparator, + significanceFactors, + significantRunsCollector), + new SearchEndpoint(benchmarkAccess, commitAccess, repoAccess), + new TestTokenEndpoint()) + .forEach(endpoint -> environment.jersey().register(endpoint)); + } + + private void configureMetrics(Environment environment) { + PrometheusMeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); + + registry.config().commonTags("application", "Velcom"); + + Metrics.globalRegistry.add(registry); + + Metrics.globalRegistry + .config() + .meterFilter( + new MeterFilter() { + @Override + public MeterFilterReply accept(Id id) { + return MeterFilterReply.NEUTRAL; + } + + @Override + public Id map(Id id) { + final String cacheTag = id.getTag("cache"); + if (cacheTag == null) { + return id; + } + + // meter is a cache meter => prepend cacheTag to name + return new Id( + cacheTag + "_" + id.getName(), + Tags.of(id.getTags()), + id.getBaseUnit(), + id.getDescription(), + id.getType()); + } + }); + + try { + Class.forName("org.aspectj.weaver.WeaverMessages"); + LOGGER.info("AspectJ weaver agent detected, providing detailed timing metrics"); + Gauge.builder("aspectj.weaver.enabled", () -> 1).register(registry); + } catch (ClassNotFoundException e) { + LOGGER.warn("AspectJ weaver agent NOT FOUND! Many metrics will NOT be available"); + Gauge.builder("aspectj.weaver.enabled", () -> 0).register(registry); + } + + new ClassLoaderMetrics().bindTo(Metrics.globalRegistry); + new JvmCompilationMetrics().bindTo(Metrics.globalRegistry); + new JvmGcMetrics().bindTo(Metrics.globalRegistry); + new JvmHeapPressureMetrics().bindTo(Metrics.globalRegistry); + new JvmMemoryMetrics().bindTo(Metrics.globalRegistry); + new JvmThreadMetrics().bindTo(Metrics.globalRegistry); + new ProcessorMetrics().bindTo(Metrics.globalRegistry); + new UptimeMetrics().bindTo(Metrics.globalRegistry); + + environment + .admin() + .addServlet( + "prometheusMetrics", + new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.getWriter().write(registry.scrape()); + } + }) + .addMapping("/prometheusMetrics"); + } + + /** + * Since we've never used and don't plan on using health checks any time soon, this function + * disables the big scary warning dropwizard shows on startup. + */ + private void configureDummyHealthCheck(Environment environment) { + environment + .healthChecks() + .register( + "dummy", + new HealthCheck() { + @Override + protected Result check() { + return Result.healthy(); + } + }); + } + + private void configureSerialization(Environment environment) { + // This mapper should be configured the same as the one in SerializingTest.java + environment + .getObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .setSerializationInclusion(Include.NON_NULL); + } + + private void configureExceptionMappers(Environment environment) { + environment.jersey().register(new InvalidQueryParamsExceptionMapper()); + environment.jersey().register(new NoSuchCommitExceptionMapper()); + environment.jersey().register(new NoSuchDimensionExceptionMapper()); + environment.jersey().register(new NoSuchRepoExceptionMapper()); + environment.jersey().register(new NoSuchRunExceptionMapper()); + environment.jersey().register(new NoSuchTaskExceptionMapper()); + environment.jersey().register(new TaskAlreadyExistsExceptionMapper()); + environment.jersey().register(new ArgumentParseExceptionMapper()); + } + + private void configureAuthentication(Environment environment, String adminToken) { + environment + .jersey() + .register( + new AuthDynamicFeature( + new BasicCredentialAuthFilter.Builder() + .setAuthenticator(new AdminAuthenticator(adminToken)) + .buildAuthFilter())); + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(Admin.class)); + } + + private void configureCors(Environment environment) { + var filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class); + filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*"); + filter.setInitParameter( + CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,PUT,POST,DELETE,OPTIONS,PATCH"); + filter.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); + filter.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*"); + filter.setInitParameter( + "allowedHeaders", + "Content-Type,Authorization,X-Requested-With,Content-Length,Accept-Encoding,Origin"); + filter.setInitParameter("allowCredentials", "true"); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/AccessUtils.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/AccessUtils.java index 945a2123b..cbb6920f2 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/AccessUtils.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/AccessUtils.java @@ -8,55 +8,45 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * A utility class containing helper functions specific to the access layer. - */ +/** A utility class containing helper functions specific to the access layer. */ public class AccessUtils { - private AccessUtils() { - throw new UnsupportedOperationException(); - } - - /** - * Interpret a repo id, commit hash and tar description as a source. - * - *

If a commit hash is present, the source is a {@link CommitSource}, otherwise it is a - * {@link TarSource}. If it is a {@link CommitSource}, the repo id must also be present and the - * tar description must not be present. If it is a {@link TarSource}, the repo id may or may not - * be present. - * - * @param repoId the repo id - * @param commitHash the commit hash - * @param tarDesc the tar description - * @return the source - */ - public static Either readSource(@Nullable String repoId, - @Nullable String commitHash, @Nullable String tarDesc) { - - if (commitHash != null) { // Must be commit source - if (repoId == null) { - throw new IllegalArgumentException("repoId must not be null if commit hash is not null"); - } - if (tarDesc != null) { - throw new IllegalArgumentException("tarDesc must be null if commit hash is not null"); - } - - return Either.ofLeft(new CommitSource( - RepoId.fromString(repoId), - new CommitHash(commitHash) - )); - } else { // Must be tar source - if (tarDesc == null) { - throw new IllegalArgumentException("tarDesc must not be null if commit hash is null"); - } - - return Either.ofRight(new TarSource( - tarDesc, - Optional.ofNullable(repoId) - .map(RepoId::fromString) - .orElse(null) - )); - } - } - + private AccessUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Interpret a repo id, commit hash and tar description as a source. + * + *

If a commit hash is present, the source is a {@link CommitSource}, otherwise it is a {@link + * TarSource}. If it is a {@link CommitSource}, the repo id must also be present and the tar + * description must not be present. If it is a {@link TarSource}, the repo id may or may not be + * present. + * + * @param repoId the repo id + * @param commitHash the commit hash + * @param tarDesc the tar description + * @return the source + */ + public static Either readSource( + @Nullable String repoId, @Nullable String commitHash, @Nullable String tarDesc) { + + if (commitHash != null) { // Must be commit source + if (repoId == null) { + throw new IllegalArgumentException("repoId must not be null if commit hash is not null"); + } + if (tarDesc != null) { + throw new IllegalArgumentException("tarDesc must be null if commit hash is not null"); + } + + return Either.ofLeft(new CommitSource(RepoId.fromString(repoId), new CommitHash(commitHash))); + } else { // Must be tar source + if (tarDesc == null) { + throw new IllegalArgumentException("tarDesc must not be null if commit hash is null"); + } + + return Either.ofRight( + new TarSource(tarDesc, Optional.ofNullable(repoId).map(RepoId::fromString).orElse(null))); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/ArchiveReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/ArchiveReadAccess.java index d08b6e8c5..b73f98d49 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/ArchiveReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/ArchiveReadAccess.java @@ -24,151 +24,146 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * An access for retrieving tar archives for tasks and for the bench repo. - */ +/** An access for retrieving tar archives for tasks and for the bench repo. */ public class ArchiveReadAccess { - private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveReadAccess.class); - private static final String BENCH_REPO_DIR_NAME = "benchrepo"; - - private final Path rootDir; - private final RepoStorage repoStorage; - private final TarFileStorage tarFileStorage; - private final String benchRepoUrl; - - private final AtomicInteger nextArchivePathNr; - - public ArchiveReadAccess(Path rootDir, RepoStorage repoStorage, TarFileStorage tarFileStorage, - String benchRepoUrl) throws IOException { - - this.rootDir = rootDir; - this.repoStorage = repoStorage; - this.tarFileStorage = tarFileStorage; - this.benchRepoUrl = benchRepoUrl; - - nextArchivePathNr = new AtomicInteger(0); - - Files.createDirectories(rootDir); - } - - /** - * Transfer the tar file for a task to an {@link OutputStream}. Creates the tar file if - * necessary. - * - * @param task the task whose tar file to transfer - * @param outputStream the tar file is transferred to this stream - * @throws TarRetrieveException if the tar file could not be retrieved - * @throws TarTransferException if the tar file could not be transferred - */ - public void transferTask(Task task, OutputStream outputStream) - throws TarRetrieveException, TarTransferException { - if (task.getSource().getLeft().isPresent()) { - CommitSource commitSource = task.getSource().getLeft().get(); - transferCommitTask(task, commitSource, outputStream); - } else { - transferTarTask(task, outputStream); - } - } - - /** - * Returns a new unique directory name to clone a repo into. Guaranteed to return a different name - * every call. Is only based on the repo dir name and commit hash so the directory names are more - * human-readable for debugging purposes. - * - * @param repoDirName the name of the repo's directory - * @param commitHash the commit hash that will be checked out here - * @return a new unique name that should be safe to use as a directory name - */ - private Path getArchivePath(String repoDirName, CommitHash commitHash) { - String hashAsString = commitHash.getHash(); - String numberPart = Integer.toString(nextArchivePathNr.getAndIncrement()); - return rootDir.resolve(repoDirName + "_" + hashAsString + "_" + numberPart); - } - - private void transferCommitTask(Task task, CommitSource commitSource, OutputStream outputStream) - throws TarRetrieveException, TarTransferException { - - transferCommit( - task, - commitSource.getRepoId().getDirectoryName(), - commitSource.getHash(), - outputStream - ); - } - - private void transferCommit(@Nullable Task task, String repoDirName, CommitHash commitHash, - OutputStream outputStream) throws TarRetrieveException, TarTransferException { - - Path archivePath = getArchivePath(repoDirName, commitHash); - - try { - try { - TransferUtils.cloneRepo(repoStorage, repoDirName, archivePath, commitHash); - } catch (RepositoryAcquisitionException | CloneException | IOException e) { - throw new TarRetrieveException(e, task); - } - - try { - TransferUtils.tarRepo(archivePath, outputStream); - } catch (IOException e) { - throw new TarTransferException(e, task); - } - } finally { - try { - FileHelper.deleteDirectoryOrFile(archivePath); - } catch (IOException e) { - LOGGER.warn("Failed to delete " + archivePath); - } - } - } - - private void transferTarTask(Task task, OutputStream outputStream) throws TarTransferException { - try { - tarFileStorage.retrieveTarFile(task.getIdAsString(), outputStream); - } catch (IOException e) { - throw new TarTransferException(e, task); - } - } - - // Bench repo functions - - public String getBenchRepoDirName() { - return BENCH_REPO_DIR_NAME; - } - - public String getBenchRepoRemoteUrl() { - return benchRepoUrl; - } - - /** - * @return the bench repo's current hash or {@link Optional#empty()} if it could not be retrieved - */ - public Optional getBenchRepoCommitHash() { - try (Repository repository = repoStorage.acquireRepository(BENCH_REPO_DIR_NAME)) { - return Optional.ofNullable(repository.resolve(Constants.HEAD)) - .map(AnyObjectId::getName) - .map(CommitHash::new); - } catch (RepositoryAcquisitionException | IOException e) { - return Optional.empty(); - } - } - - /** - * Transfer the bench repo's current state as a tar file to the output stream. - * - * @param outputStream the tar file is transferred to this stream - * @throws TarRetrieveException if the tar file could not be retrieved - * @throws TarTransferException if the tar file could not be transferred - */ - public void transferBenchRepo(OutputStream outputStream) - throws TarRetrieveException, TarTransferException { - - Optional hash = getBenchRepoCommitHash(); - if (hash.isPresent()) { - transferCommit(null, BENCH_REPO_DIR_NAME, hash.get(), outputStream); - } else { - throw new TarRetrieveException(); - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveReadAccess.class); + private static final String BENCH_REPO_DIR_NAME = "benchrepo"; + + private final Path rootDir; + private final RepoStorage repoStorage; + private final TarFileStorage tarFileStorage; + private final String benchRepoUrl; + + private final AtomicInteger nextArchivePathNr; + + public ArchiveReadAccess( + Path rootDir, RepoStorage repoStorage, TarFileStorage tarFileStorage, String benchRepoUrl) + throws IOException { + + this.rootDir = rootDir; + this.repoStorage = repoStorage; + this.tarFileStorage = tarFileStorage; + this.benchRepoUrl = benchRepoUrl; + + nextArchivePathNr = new AtomicInteger(0); + + Files.createDirectories(rootDir); + } + + /** + * Transfer the tar file for a task to an {@link OutputStream}. Creates the tar file if necessary. + * + * @param task the task whose tar file to transfer + * @param outputStream the tar file is transferred to this stream + * @throws TarRetrieveException if the tar file could not be retrieved + * @throws TarTransferException if the tar file could not be transferred + */ + public void transferTask(Task task, OutputStream outputStream) + throws TarRetrieveException, TarTransferException { + if (task.getSource().getLeft().isPresent()) { + CommitSource commitSource = task.getSource().getLeft().get(); + transferCommitTask(task, commitSource, outputStream); + } else { + transferTarTask(task, outputStream); + } + } + + /** + * Returns a new unique directory name to clone a repo into. Guaranteed to return a different name + * every call. Is only based on the repo dir name and commit hash so the directory names are more + * human-readable for debugging purposes. + * + * @param repoDirName the name of the repo's directory + * @param commitHash the commit hash that will be checked out here + * @return a new unique name that should be safe to use as a directory name + */ + private Path getArchivePath(String repoDirName, CommitHash commitHash) { + String hashAsString = commitHash.getHash(); + String numberPart = Integer.toString(nextArchivePathNr.getAndIncrement()); + return rootDir.resolve(repoDirName + "_" + hashAsString + "_" + numberPart); + } + + private void transferCommitTask(Task task, CommitSource commitSource, OutputStream outputStream) + throws TarRetrieveException, TarTransferException { + + transferCommit( + task, commitSource.getRepoId().getDirectoryName(), commitSource.getHash(), outputStream); + } + + private void transferCommit( + @Nullable Task task, String repoDirName, CommitHash commitHash, OutputStream outputStream) + throws TarRetrieveException, TarTransferException { + + Path archivePath = getArchivePath(repoDirName, commitHash); + + try { + try { + TransferUtils.cloneRepo(repoStorage, repoDirName, archivePath, commitHash); + } catch (RepositoryAcquisitionException | CloneException | IOException e) { + throw new TarRetrieveException(e, task); + } + + try { + TransferUtils.tarRepo(archivePath, outputStream); + } catch (IOException e) { + throw new TarTransferException(e, task); + } + } finally { + try { + FileHelper.deleteDirectoryOrFile(archivePath); + } catch (IOException e) { + LOGGER.warn("Failed to delete " + archivePath); + } + } + } + + private void transferTarTask(Task task, OutputStream outputStream) throws TarTransferException { + try { + tarFileStorage.retrieveTarFile(task.getIdAsString(), outputStream); + } catch (IOException e) { + throw new TarTransferException(e, task); + } + } + + // Bench repo functions + + public String getBenchRepoDirName() { + return BENCH_REPO_DIR_NAME; + } + + public String getBenchRepoRemoteUrl() { + return benchRepoUrl; + } + + /** + * @return the bench repo's current hash or {@link Optional#empty()} if it could not be retrieved + */ + public Optional getBenchRepoCommitHash() { + try (Repository repository = repoStorage.acquireRepository(BENCH_REPO_DIR_NAME)) { + return Optional.ofNullable(repository.resolve(Constants.HEAD)) + .map(AnyObjectId::getName) + .map(CommitHash::new); + } catch (RepositoryAcquisitionException | IOException e) { + return Optional.empty(); + } + } + + /** + * Transfer the bench repo's current state as a tar file to the output stream. + * + * @param outputStream the tar file is transferred to this stream + * @throws TarRetrieveException if the tar file could not be retrieved + * @throws TarTransferException if the tar file could not be transferred + */ + public void transferBenchRepo(OutputStream outputStream) + throws TarRetrieveException, TarTransferException { + + Optional hash = getBenchRepoCommitHash(); + if (hash.isPresent()) { + transferCommit(null, BENCH_REPO_DIR_NAME, hash.get(), outputStream); + } else { + throw new TarRetrieveException(); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarRetrieveException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarRetrieveException.java index da8899672..5a53a5dbd 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarRetrieveException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarRetrieveException.java @@ -4,37 +4,34 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * Thrown when something goes wrong while preparing for the transfer of a tar file. - */ +/** Thrown when something goes wrong while preparing for the transfer of a tar file. */ public class TarRetrieveException extends Exception { - @Nullable - private final Task task; - - private static String makeMessage(@Nullable Task task) { - if (task == null) { - return "Failed to retrieve tar file"; - } else { - return "Failed to retrieve tar file for task " + task; - } - } - - public TarRetrieveException(Throwable t, @Nullable Task task) { - super(makeMessage(task), t); - this.task = task; - } - - public TarRetrieveException(@Nullable Task task) { - super(makeMessage(task)); - this.task = task; - } - - public TarRetrieveException() { - this(null); - } - - public Optional getTask() { - return Optional.ofNullable(task); - } + @Nullable private final Task task; + + private static String makeMessage(@Nullable Task task) { + if (task == null) { + return "Failed to retrieve tar file"; + } else { + return "Failed to retrieve tar file for task " + task; + } + } + + public TarRetrieveException(Throwable t, @Nullable Task task) { + super(makeMessage(task), t); + this.task = task; + } + + public TarRetrieveException(@Nullable Task task) { + super(makeMessage(task)); + this.task = task; + } + + public TarRetrieveException() { + this(null); + } + + public Optional getTask() { + return Optional.ofNullable(task); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarTransferException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarTransferException.java index e084bb640..6299b7fd3 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarTransferException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/archiveaccess/exceptions/TarTransferException.java @@ -4,37 +4,34 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * Thrown when something goes wrong during the transfer of a tar file. - */ +/** Thrown when something goes wrong during the transfer of a tar file. */ public class TarTransferException extends Exception { - @Nullable - private final Task task; - - private static String makeMessage(@Nullable Task task) { - if (task == null) { - return "Failed to transfer tar file"; - } else { - return "Failed to transfer tar file for task " + task; - } - } - - public TarTransferException(Throwable t, @Nullable Task task) { - super(makeMessage(task), t); - this.task = task; - } - - public TarTransferException(@Nullable Task task) { - super(makeMessage(task)); - this.task = task; - } - - public TarTransferException() { - this(null); - } - - public Optional getTask() { - return Optional.ofNullable(task); - } + @Nullable private final Task task; + + private static String makeMessage(@Nullable Task task) { + if (task == null) { + return "Failed to transfer tar file"; + } else { + return "Failed to transfer tar file for task " + task; + } + } + + public TarTransferException(Throwable t, @Nullable Task task) { + super(makeMessage(task), t); + this.task = task; + } + + public TarTransferException(@Nullable Task task) { + super(makeMessage(task)); + this.task = task; + } + + public TarTransferException() { + this(null); + } + + public Optional getTask() { + return Optional.ofNullable(task); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccess.java index d0102477a..72f876828 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccess.java @@ -48,388 +48,368 @@ import org.jooq.Record1; import org.jooq.codegen.db.tables.records.RunRecord; -/** - * Access for retrieving runs and their measurements. - */ +/** Access for retrieving runs and their measurements. */ public class BenchmarkReadAccess { - - protected final DatabaseStorage databaseStorage; - - public BenchmarkReadAccess(DatabaseStorage databaseStorage) { - this.databaseStorage = databaseStorage; - } - - private List loadRuns(DBReadAccess db, List runRecords) { - if (runRecords.isEmpty()) { - return List.of(); - } - - // 0. Preparation - Set runIds = runRecords.stream() - .map(RunRecord::getId) - .collect(toSet()); - - // 1. Load measurement values (key: measurement id) - Map> values = db.dsl() - .select(MEASUREMENT.ID, MEASUREMENT_VALUE.VALUE) - .from(MEASUREMENT) - .join(MEASUREMENT_VALUE) - .on(MEASUREMENT_VALUE.MEASUREMENT_ID.eq(MEASUREMENT.ID)) - .where(MEASUREMENT.RUN_ID.in(runIds)) - .fetchGroups(MEASUREMENT.ID, MEASUREMENT_VALUE.VALUE); - - // 2. Load measurements - Map> measurements = db.dsl() - .selectFrom(MEASUREMENT) - .where(MEASUREMENT.RUN_ID.in(runIds)) - .stream() - .map(record -> { - final Either content; - if (record.getError() != null) { - content = Either.ofLeft(new MeasurementError(record.getError())); - } else { - content = Either.ofRight(new MeasurementValues(values.get(record.getId()))); - } - - return new Measurement( - RunId.fromString(record.getRunId()), - new Dimension(record.getBenchmark(), record.getMetric()), - content - ); - }) - .collect(groupingBy(Measurement::getRunId)); - - // 3. Create run entities - return runRecords.stream() - .map(record -> { - RunId runId = RunId.fromString(record.getId()); - - final Either source = AccessUtils.readSource( - record.getRepoId(), - record.getCommitHash(), - record.getTarDesc() - ); - - final Either> result; - if (record.getError() != null) { - result = Either.ofLeft(new RunError( - record.getError(), - RunErrorType.fromTextualRepresentation(record.getErrorType()) - )); - } else { - result = Either.ofRight(measurements.getOrDefault(runId, List.of())); - } - - return new Run( - runId, - record.getAuthor(), - record.getRunnerName(), - record.getRunnerInfo(), - record.getStartTime(), - record.getStopTime(), - source, - result - ); - }) - .collect(toList()); - } - - /** - * Get a run by its id. - * - * @param runId the run's id - * @return the run, if one exists - * @throws NoSuchRunException if no run with the given id is found - */ - public Run getRun(RunId runId) throws NoSuchRunException { - List runs = getRuns(List.of(runId)); - if (runs.size() == 1) { - return runs.get(0); - } else { - throw new NoSuchRunException(runId); - } - } - - /** - * Get multiple runs by their ids. - * - * @param runIds the ids of the runs to retrieve - * @return a list containing only those of the runs that exist, in no particular order - */ - public List getRuns(Collection runIds) { - Set runIdStrings = runIds.stream() - .map(RunId::getIdAsString) - .collect(toSet()); - - return databaseStorage.acquireReadTransaction(db -> { - List runRecords = db.dsl() - .selectFrom(RUN) - .where(RUN.ID.in(runIdStrings)) - .stream() - .collect(toList()); - - return loadRuns(db, runRecords); - }); - } - - /** - * Get all runs of a specific commit ordered by their start time. - * - * @param repoId the id of the repository the commit is in - * @param commitHash the hash of the commit - * @return all runs for the commit, ordered from new to old - */ - public List getAllRunIds(RepoId repoId, CommitHash commitHash) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(RUN) - .where(RUN.REPO_ID.eq(repoId.getIdAsString())) - .and(RUN.COMMIT_HASH.eq(commitHash.getHash())) - .orderBy(RUN.START_TIME.desc()) - .stream() - .map(RunRecord::getId) - .map(RunId::fromString) - .collect(toList()); - } - } - - /** - * Get the ids of the most recent runs ordered by their commit's committer time. Runs with no - * associated commit are ignored. If a commit has multiple runs, they are ordered from new to - * old. - * - * @param skip how many recent runs to skip - * @param amount how many recent runs to collect - * @return a sorted list containing the recent runs from new to old - * @throws IllegalArgumentException if skip/amount is negative - */ - public List getRecentRunIds(int skip, int amount) throws IllegalArgumentException { - if (skip < 0) { - throw new IllegalArgumentException("skip must be positive"); - } - if (amount < 0) { - throw new IllegalArgumentException("amount must be positive"); - } - if (amount == 0) { - return Collections.emptyList(); - } - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(RUN.ID) - .from(RUN) - .join(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) - .where(KNOWN_COMMIT.TRACKED) - .orderBy(KNOWN_COMMIT.COMMITTER_DATE.desc(), RUN.START_TIME.desc()) - .limit(skip, amount) - .stream() - .map(Record1::value1) - .map(RunId::fromString) - .collect(toList()); - } - } - - /** - * Find the id of a commit's latest run. - * - * @param repoId the commit's repo - * @param commitHash the commit's hash - * @return the id of the commit's latest run, or empty if the commit has no associated run - */ - public Optional getLatestRunId(RepoId repoId, CommitHash commitHash) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(LATEST_RUN) - .where(LATEST_RUN.REPO_ID.eq(repoId.getIdAsString())) - .and(LATEST_RUN.COMMIT_HASH.eq(commitHash.getHash())) - .fetchOptional() - .map(record -> RunId.fromString(record.getId())); - } - } - - /** - * Find the id of the latest run for each commit. - * - * @param repoId the repo the commits are in - * @param commitHashes the commits whose latest runs to find - * @return a map containing each commit's latest run's id. If a commit has not associated run, it - * is omitted from this map. - */ - public Map getLatestRunIds(RepoId repoId, - Collection commitHashes) { - - Set hashStrings = commitHashes.stream() - .map(CommitHash::getHash) - .collect(toSet()); - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(LATEST_RUN) - .where(LATEST_RUN.REPO_ID.eq(repoId.getIdAsString())) - .and(LATEST_RUN.COMMIT_HASH.in(hashStrings)) - .stream() - .collect(toMap( - record -> new CommitHash(record.getCommitHash()), - record -> RunId.fromString(record.getId()) - )); - } - } - - public List searchRuns(Integer limit, - @Nullable RepoId repoId, String queryStr) { - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - var query = db.dsl() - .select( - RUN.ID, - RUN.REPO_ID, - KNOWN_COMMIT.MESSAGE, - RUN.TAR_DESC, - RUN.START_TIME, - RUN.STOP_TIME - ) - .from(RUN) - .leftOuterJoin(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID)) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH)) - .where(RUN.ID.contains(queryStr) - .or(RUN.AUTHOR.contains(queryStr)) - .or(RUN.RUNNER_NAME.contains(queryStr)) - .or(RUN.RUNNER_INFO.contains(queryStr)) - .or(RUN.TAR_DESC.contains(queryStr))); - - if (repoId != null) { - query = query.and(RUN.REPO_ID.eq(repoId.getIdAsString())); - } - - return query.orderBy(RUN.START_TIME.desc()) - .limit(limit) - .stream() - .map(record -> new SearchRunDescription( - RunId.fromString(record.value1()), - Optional.ofNullable(record.value2()).map(RepoId::fromString).orElse(null), - Optional.ofNullable(record.value3()) - .map(Commit::splitMessageIntoSections) - .map(Pair::getFirst) - .orElse(null), - record.value4(), - record.value5(), - record.value6() - )) - .collect(toList()); - } - } - - public ShortRunDescription getShortRunDescription(RunId runId) throws NoSuchRunException { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(RUN.COMMIT_HASH, KNOWN_COMMIT.MESSAGE, RUN.TAR_DESC) - .from(RUN) - .leftJoin(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID)) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH)) - .where(RUN.ID.eq(runId.getIdAsString())) - .fetchOptional() - .map(record -> new ShortRunDescription( - runId, - record.value1(), - record.value2(), - record.value3() - )) - .orElseThrow(() -> new NoSuchRunException(runId)); - } - } - - public long getAmountOfOutdatedRuns() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - Field namedField = field("n", Integer.class); - return db.dsl() - .select(sum(namedField)) - .from(select(count().minus(1).as(namedField)) - .from(RUN) - .where(RUN.COMMIT_HASH.isNotNull()) - .groupBy(RUN.COMMIT_HASH)) - .fetchSingle() - .value1() - .longValue(); - } - } - - public long getAmountOfTarRunsNotAttachedToRepo() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(count()) - .from(RUN) - .where(RUN.COMMIT_HASH.isNull()) - .and(RUN.REPO_ID.isNull()) - .fetchSingle() - .value1(); - } - } - - public long getAmountOfTarRunsAttachedToRepo() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(count()) - .from(RUN) - .where(RUN.COMMIT_HASH.isNull()) - .and(RUN.REPO_ID.isNotNull()) - .fetchSingle() - .value1(); - } - } - - public long getAmountOfFailedRuns() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(count()) - .from(RUN) - .where(RUN.ERROR.isNotNull()) - .fetchSingle() - .value1(); - } - } - - public long getAmountOfFailedRunErrorMsgChars() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(sum(length(RUN.ERROR))) - .from(RUN) - .where(RUN.ERROR.isNotNull()) - .fetchSingle() - .value1() - .longValue(); - } - } - - public long getAmountOfRunsForUntrackedCommits() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(count()) - .from(RUN) - .join(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) - .where(not(KNOWN_COMMIT.TRACKED)) - .fetchSingle() - .value1(); - } - } - - public long getAmountOfRunsForUnreachableCommits() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(count()) - .from(RUN) - .join(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) - .where(not(KNOWN_COMMIT.REACHABLE)) - .fetchSingle() - .value1(); - } - } + protected final DatabaseStorage databaseStorage; + + public BenchmarkReadAccess(DatabaseStorage databaseStorage) { + this.databaseStorage = databaseStorage; + } + + private List loadRuns(DBReadAccess db, List runRecords) { + if (runRecords.isEmpty()) { + return List.of(); + } + + // 0. Preparation + Set runIds = runRecords.stream().map(RunRecord::getId).collect(toSet()); + + // 1. Load measurement values (key: measurement id) + Map> values = + db.dsl() + .select(MEASUREMENT.ID, MEASUREMENT_VALUE.VALUE) + .from(MEASUREMENT) + .join(MEASUREMENT_VALUE) + .on(MEASUREMENT_VALUE.MEASUREMENT_ID.eq(MEASUREMENT.ID)) + .where(MEASUREMENT.RUN_ID.in(runIds)) + .fetchGroups(MEASUREMENT.ID, MEASUREMENT_VALUE.VALUE); + + // 2. Load measurements + Map> measurements = + db.dsl().selectFrom(MEASUREMENT).where(MEASUREMENT.RUN_ID.in(runIds)).stream() + .map( + record -> { + final Either content; + if (record.getError() != null) { + content = Either.ofLeft(new MeasurementError(record.getError())); + } else { + content = Either.ofRight(new MeasurementValues(values.get(record.getId()))); + } + + return new Measurement( + RunId.fromString(record.getRunId()), + new Dimension(record.getBenchmark(), record.getMetric()), + content); + }) + .collect(groupingBy(Measurement::getRunId)); + + // 3. Create run entities + return runRecords.stream() + .map( + record -> { + RunId runId = RunId.fromString(record.getId()); + + final Either source = + AccessUtils.readSource( + record.getRepoId(), record.getCommitHash(), record.getTarDesc()); + + final Either> result; + if (record.getError() != null) { + result = + Either.ofLeft( + new RunError( + record.getError(), + RunErrorType.fromTextualRepresentation(record.getErrorType()))); + } else { + result = Either.ofRight(measurements.getOrDefault(runId, List.of())); + } + + return new Run( + runId, + record.getAuthor(), + record.getRunnerName(), + record.getRunnerInfo(), + record.getStartTime(), + record.getStopTime(), + source, + result); + }) + .collect(toList()); + } + + /** + * Get a run by its id. + * + * @param runId the run's id + * @return the run, if one exists + * @throws NoSuchRunException if no run with the given id is found + */ + public Run getRun(RunId runId) throws NoSuchRunException { + List runs = getRuns(List.of(runId)); + if (runs.size() == 1) { + return runs.get(0); + } else { + throw new NoSuchRunException(runId); + } + } + + /** + * Get multiple runs by their ids. + * + * @param runIds the ids of the runs to retrieve + * @return a list containing only those of the runs that exist, in no particular order + */ + public List getRuns(Collection runIds) { + Set runIdStrings = runIds.stream().map(RunId::getIdAsString).collect(toSet()); + + return databaseStorage.acquireReadTransaction( + db -> { + List runRecords = + db.dsl().selectFrom(RUN).where(RUN.ID.in(runIdStrings)).stream().collect(toList()); + + return loadRuns(db, runRecords); + }); + } + + /** + * Get all runs of a specific commit ordered by their start time. + * + * @param repoId the id of the repository the commit is in + * @param commitHash the hash of the commit + * @return all runs for the commit, ordered from new to old + */ + public List getAllRunIds(RepoId repoId, CommitHash commitHash) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .selectFrom(RUN) + .where(RUN.REPO_ID.eq(repoId.getIdAsString())) + .and(RUN.COMMIT_HASH.eq(commitHash.getHash())) + .orderBy(RUN.START_TIME.desc()) + .stream() + .map(RunRecord::getId) + .map(RunId::fromString) + .collect(toList()); + } + } + + /** + * Get the ids of the most recent runs ordered by their commit's committer time. Runs with no + * associated commit are ignored. If a commit has multiple runs, they are ordered from new to old. + * + * @param skip how many recent runs to skip + * @param amount how many recent runs to collect + * @return a sorted list containing the recent runs from new to old + * @throws IllegalArgumentException if skip/amount is negative + */ + public List getRecentRunIds(int skip, int amount) throws IllegalArgumentException { + if (skip < 0) { + throw new IllegalArgumentException("skip must be positive"); + } + if (amount < 0) { + throw new IllegalArgumentException("amount must be positive"); + } + if (amount == 0) { + return Collections.emptyList(); + } + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .select(RUN.ID) + .from(RUN) + .join(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID).and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) + .where(KNOWN_COMMIT.TRACKED) + .orderBy(KNOWN_COMMIT.COMMITTER_DATE.desc(), RUN.START_TIME.desc()) + .limit(skip, amount) + .stream() + .map(Record1::value1) + .map(RunId::fromString) + .collect(toList()); + } + } + + /** + * Find the id of a commit's latest run. + * + * @param repoId the commit's repo + * @param commitHash the commit's hash + * @return the id of the commit's latest run, or empty if the commit has no associated run + */ + public Optional getLatestRunId(RepoId repoId, CommitHash commitHash) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .selectFrom(LATEST_RUN) + .where(LATEST_RUN.REPO_ID.eq(repoId.getIdAsString())) + .and(LATEST_RUN.COMMIT_HASH.eq(commitHash.getHash())) + .fetchOptional() + .map(record -> RunId.fromString(record.getId())); + } + } + + /** + * Find the id of the latest run for each commit. + * + * @param repoId the repo the commits are in + * @param commitHashes the commits whose latest runs to find + * @return a map containing each commit's latest run's id. If a commit has not associated run, it + * is omitted from this map. + */ + public Map getLatestRunIds( + RepoId repoId, Collection commitHashes) { + + Set hashStrings = commitHashes.stream().map(CommitHash::getHash).collect(toSet()); + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .selectFrom(LATEST_RUN) + .where(LATEST_RUN.REPO_ID.eq(repoId.getIdAsString())) + .and(LATEST_RUN.COMMIT_HASH.in(hashStrings)) + .stream() + .collect( + toMap( + record -> new CommitHash(record.getCommitHash()), + record -> RunId.fromString(record.getId()))); + } + } + + public List searchRuns( + Integer limit, @Nullable RepoId repoId, String queryStr) { + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + var query = + db.dsl() + .select( + RUN.ID, + RUN.REPO_ID, + KNOWN_COMMIT.MESSAGE, + RUN.TAR_DESC, + RUN.START_TIME, + RUN.STOP_TIME) + .from(RUN) + .leftOuterJoin(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID)) + .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH)) + .where( + RUN.ID + .contains(queryStr) + .or(RUN.AUTHOR.contains(queryStr)) + .or(RUN.RUNNER_NAME.contains(queryStr)) + .or(RUN.RUNNER_INFO.contains(queryStr)) + .or(RUN.TAR_DESC.contains(queryStr))); + + if (repoId != null) { + query = query.and(RUN.REPO_ID.eq(repoId.getIdAsString())); + } + + return query.orderBy(RUN.START_TIME.desc()).limit(limit).stream() + .map( + record -> + new SearchRunDescription( + RunId.fromString(record.value1()), + Optional.ofNullable(record.value2()).map(RepoId::fromString).orElse(null), + Optional.ofNullable(record.value3()) + .map(Commit::splitMessageIntoSections) + .map(Pair::getFirst) + .orElse(null), + record.value4(), + record.value5(), + record.value6())) + .collect(toList()); + } + } + + public ShortRunDescription getShortRunDescription(RunId runId) throws NoSuchRunException { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(RUN.COMMIT_HASH, KNOWN_COMMIT.MESSAGE, RUN.TAR_DESC) + .from(RUN) + .leftJoin(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID)) + .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH)) + .where(RUN.ID.eq(runId.getIdAsString())) + .fetchOptional() + .map( + record -> + new ShortRunDescription(runId, record.value1(), record.value2(), record.value3())) + .orElseThrow(() -> new NoSuchRunException(runId)); + } + } + + public long getAmountOfOutdatedRuns() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + Field namedField = field("n", Integer.class); + return db.dsl() + .select(sum(namedField)) + .from( + select(count().minus(1).as(namedField)) + .from(RUN) + .where(RUN.COMMIT_HASH.isNotNull()) + .groupBy(RUN.COMMIT_HASH)) + .fetchSingle() + .value1() + .longValue(); + } + } + + public long getAmountOfTarRunsNotAttachedToRepo() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(count()) + .from(RUN) + .where(RUN.COMMIT_HASH.isNull()) + .and(RUN.REPO_ID.isNull()) + .fetchSingle() + .value1(); + } + } + + public long getAmountOfTarRunsAttachedToRepo() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(count()) + .from(RUN) + .where(RUN.COMMIT_HASH.isNull()) + .and(RUN.REPO_ID.isNotNull()) + .fetchSingle() + .value1(); + } + } + + public long getAmountOfFailedRuns() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl().select(count()).from(RUN).where(RUN.ERROR.isNotNull()).fetchSingle().value1(); + } + } + + public long getAmountOfFailedRunErrorMsgChars() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(sum(length(RUN.ERROR))) + .from(RUN) + .where(RUN.ERROR.isNotNull()) + .fetchSingle() + .value1() + .longValue(); + } + } + + public long getAmountOfRunsForUntrackedCommits() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(count()) + .from(RUN) + .join(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID).and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) + .where(not(KNOWN_COMMIT.TRACKED)) + .fetchSingle() + .value1(); + } + } + + public long getAmountOfRunsForUnreachableCommits() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(count()) + .from(RUN) + .join(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID).and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) + .where(not(KNOWN_COMMIT.REACHABLE)) + .fetchSingle() + .value1(); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccess.java index ee965b001..5ebe0803c 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccess.java @@ -32,153 +32,152 @@ import org.jooq.codegen.db.tables.records.MeasurementValueRecord; import org.jooq.codegen.db.tables.records.RunRecord; -/** - * Access for adding new runs. - */ +/** Access for adding new runs. */ public class BenchmarkWriteAccess extends BenchmarkReadAccess { - private final AvailableDimensionsCache availableDimensionsCache; - private final LatestRunCache latestRunCache; - - public BenchmarkWriteAccess(DatabaseStorage databaseStorage, - AvailableDimensionsCache availableDimensionsCache, LatestRunCache latestRunCache) { - - super(databaseStorage); - - this.availableDimensionsCache = availableDimensionsCache; - this.latestRunCache = latestRunCache; - } - - /** - * Inserts the specified run into the database. Also deletes the task with the same id from the * - * task table in the same transaction. - * - * @param newRun the run to insert - */ - public void insertRun(NewRun newRun) { - databaseStorage.acquireWriteTransaction(db -> { - deleteTask(db, newRun.getId().toTaskId()); - updateDimensions(db, newRun); - insertNewRun(db, newRun); - insertNewMeasurements(db, newRun); - }); - - newRun.getRepoId().ifPresent(availableDimensionsCache::invalidate); - newRun.getSource().getLeft().ifPresent( - commitSource -> latestRunCache.invalidate(commitSource.getRepoId(), commitSource.getHash())); - } - - private void deleteTask(DBWriteAccess db, TaskId taskId) { - db.dsl() - .deleteFrom(TASK) - .where(TASK.ID.eq(taskId.getIdAsString())) - .execute(); - } - - private void updateDimensions(DBWriteAccess db, NewRun newRun) { - Map dimensions = db.dsl() - .selectFrom(DIMENSION) - .stream() - .collect(toMap( - record -> new Dimension(record.getBenchmark(), record.getMetric()), - it -> it - )); - - newRun.getResult() - .getRight() - .orElse(List.of()) - .forEach(measurement -> { - DimensionRecord record = dimensions.get(measurement.getDimension()); - - if (record == null) { - DimensionInfo info = new DimensionInfo( - measurement.getDimension(), - measurement.getUnit().orElse(null), - measurement.getInterpretation().orElse(null) - ); - - dimensions.put(info.getDimension(), DimensionReadAccess.dimInfoToDimRecord(info)); - } else { - measurement.getUnit() - .map(Unit::getName) - .filter(it -> !record.getUnit().equals(it)) - .ifPresent(record::setUnit); - - measurement.getInterpretation() - .map(Interpretation::getTextualRepresentation) - .filter(it -> !record.getInterpretation().equals(it)) - .ifPresent(record::setInterpretation); - } - }); - - // Inserts newly generated dimensions and updates modified dimensions - db.dsl().batchStore(dimensions.values()).execute(); - } - - private void insertNewRun(DBWriteAccess db, NewRun run) { - RunRecord runRecord = new RunRecord( - run.getIdAsString(), - run.getAuthor(), - run.getRunnerName(), - run.getRunnerInfo(), - run.getStartTime(), - run.getStopTime(), - run.getRepoId() - .map(RepoId::getIdAsString) - .orElse(null), - run.getSource().getLeft() - .map(CommitSource::getHash) - .map(CommitHash::getHash) - .orElse(null), - run.getSource().getRight() - .map(TarSource::getDescription) - .orElse(null), - run.getResult().getLeft() - .map(RunError::getType) - .map(RunErrorType::getTextualRepresentation) - .orElse(null), - run.getResult().getLeft() - .map(RunError::getMessage) - .orElse(null) - ); - - db.dsl().batchInsert(runRecord).execute(); - } - - private void insertNewMeasurements(DBWriteAccess db, NewRun run) { - run.getResult() - .getRight() - .orElse(List.of()) - .forEach(measurement -> insertNewMeasurement(db, measurement)); - } - - private void insertNewMeasurement(DBWriteAccess db, NewMeasurement measurement) { - String measurementId = UUID.randomUUID().toString(); - - MeasurementRecord measurementRecord = new MeasurementRecord( - measurementId, - measurement.getRunId().getIdAsString(), - measurement.getDimension().getBenchmark(), - measurement.getDimension().getMetric(), - measurement.getUnit() - .map(Unit::getName) - .orElse(null), - measurement.getInterpretation() - .map(Interpretation::getTextualRepresentation) - .orElse(null), - measurement.getContent() - .getLeft() - .map(MeasurementError::getErrorMessage) - .orElse(null) - ); - - db.dsl().batchInsert(measurementRecord).execute(); - - List records = measurement.getContent().getRight().stream() - .flatMap(values -> values.getValues().stream() - .map(value -> new MeasurementValueRecord(measurementId, value)) - ).collect(toList()); - - db.dsl().batchInsert(records).execute(); - } + private final AvailableDimensionsCache availableDimensionsCache; + private final LatestRunCache latestRunCache; + + public BenchmarkWriteAccess( + DatabaseStorage databaseStorage, + AvailableDimensionsCache availableDimensionsCache, + LatestRunCache latestRunCache) { + + super(databaseStorage); + + this.availableDimensionsCache = availableDimensionsCache; + this.latestRunCache = latestRunCache; + } + + /** + * Inserts the specified run into the database. Also deletes the task with the same id from the * + * task table in the same transaction. + * + * @param newRun the run to insert + */ + public void insertRun(NewRun newRun) { + databaseStorage.acquireWriteTransaction( + db -> { + deleteTask(db, newRun.getId().toTaskId()); + updateDimensions(db, newRun); + insertNewRun(db, newRun); + insertNewMeasurements(db, newRun); + }); + + newRun.getRepoId().ifPresent(availableDimensionsCache::invalidate); + newRun + .getSource() + .getLeft() + .ifPresent( + commitSource -> + latestRunCache.invalidate(commitSource.getRepoId(), commitSource.getHash())); + } + + private void deleteTask(DBWriteAccess db, TaskId taskId) { + db.dsl().deleteFrom(TASK).where(TASK.ID.eq(taskId.getIdAsString())).execute(); + } + + private void updateDimensions(DBWriteAccess db, NewRun newRun) { + Map dimensions = + db.dsl().selectFrom(DIMENSION).stream() + .collect( + toMap( + record -> new Dimension(record.getBenchmark(), record.getMetric()), it -> it)); + + newRun + .getResult() + .getRight() + .orElse(List.of()) + .forEach( + measurement -> { + DimensionRecord record = dimensions.get(measurement.getDimension()); + + if (record == null) { + DimensionInfo info = + new DimensionInfo( + measurement.getDimension(), + measurement.getUnit().orElse(null), + measurement.getInterpretation().orElse(null)); + + dimensions.put(info.getDimension(), DimensionReadAccess.dimInfoToDimRecord(info)); + } else { + measurement + .getUnit() + .map(Unit::getName) + .filter(it -> !record.getUnit().equals(it)) + .ifPresent(record::setUnit); + + measurement + .getInterpretation() + .map(Interpretation::getTextualRepresentation) + .filter(it -> !record.getInterpretation().equals(it)) + .ifPresent(record::setInterpretation); + } + }); + + // Inserts newly generated dimensions and updates modified dimensions + db.dsl().batchStore(dimensions.values()).execute(); + } + + private void insertNewRun(DBWriteAccess db, NewRun run) { + RunRecord runRecord = + new RunRecord( + run.getIdAsString(), + run.getAuthor(), + run.getRunnerName(), + run.getRunnerInfo(), + run.getStartTime(), + run.getStopTime(), + run.getRepoId().map(RepoId::getIdAsString).orElse(null), + run.getSource() + .getLeft() + .map(CommitSource::getHash) + .map(CommitHash::getHash) + .orElse(null), + run.getSource().getRight().map(TarSource::getDescription).orElse(null), + run.getResult() + .getLeft() + .map(RunError::getType) + .map(RunErrorType::getTextualRepresentation) + .orElse(null), + run.getResult().getLeft().map(RunError::getMessage).orElse(null)); + + db.dsl().batchInsert(runRecord).execute(); + } + + private void insertNewMeasurements(DBWriteAccess db, NewRun run) { + run.getResult() + .getRight() + .orElse(List.of()) + .forEach(measurement -> insertNewMeasurement(db, measurement)); + } + + private void insertNewMeasurement(DBWriteAccess db, NewMeasurement measurement) { + String measurementId = UUID.randomUUID().toString(); + + MeasurementRecord measurementRecord = + new MeasurementRecord( + measurementId, + measurement.getRunId().getIdAsString(), + measurement.getDimension().getBenchmark(), + measurement.getDimension().getMetric(), + measurement.getUnit().map(Unit::getName).orElse(null), + measurement + .getInterpretation() + .map(Interpretation::getTextualRepresentation) + .orElse(null), + measurement.getContent().getLeft().map(MeasurementError::getErrorMessage).orElse(null)); + + db.dsl().batchInsert(measurementRecord).execute(); + + List records = + measurement.getContent().getRight().stream() + .flatMap( + values -> + values.getValues().stream() + .map(value -> new MeasurementValueRecord(measurementId, value))) + .collect(toList()); + + db.dsl().batchInsert(records).execute(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewMeasurement.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewMeasurement.java index a0be0b323..64261c161 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewMeasurement.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewMeasurement.java @@ -11,76 +11,89 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * An entity used for adding new runs via the BenchmarkAccess. - */ +/** An entity used for adding new runs via the BenchmarkAccess. */ public class NewMeasurement { - private final RunId runId; - private final Dimension dimension; - @Nullable - private final Unit unit; - @Nullable - private final Interpretation interpretation; - private final Either content; + private final RunId runId; + private final Dimension dimension; + @Nullable private final Unit unit; + @Nullable private final Interpretation interpretation; + private final Either content; - public NewMeasurement(RunId runId, Dimension dimension, @Nullable Unit unit, - @Nullable Interpretation interpretation, MeasurementError error) { - this(runId, dimension, unit, interpretation, Either.ofLeft(error)); - } + public NewMeasurement( + RunId runId, + Dimension dimension, + @Nullable Unit unit, + @Nullable Interpretation interpretation, + MeasurementError error) { + this(runId, dimension, unit, interpretation, Either.ofLeft(error)); + } - public NewMeasurement(RunId runId, Dimension dimension, @Nullable Unit unit, - @Nullable Interpretation interpretation, MeasurementValues values) { - this(runId, dimension, unit, interpretation, Either.ofRight(values)); - } + public NewMeasurement( + RunId runId, + Dimension dimension, + @Nullable Unit unit, + @Nullable Interpretation interpretation, + MeasurementValues values) { + this(runId, dimension, unit, interpretation, Either.ofRight(values)); + } - private NewMeasurement(RunId runId, Dimension dimension, @Nullable Unit unit, - @Nullable Interpretation interpretation, Either content) { + private NewMeasurement( + RunId runId, + Dimension dimension, + @Nullable Unit unit, + @Nullable Interpretation interpretation, + Either content) { - this.runId = runId; - this.dimension = dimension; - this.unit = unit; - this.interpretation = interpretation; - this.content = content; - } + this.runId = runId; + this.dimension = dimension; + this.unit = unit; + this.interpretation = interpretation; + this.content = content; + } - public RunId getRunId() { - return runId; - } + public RunId getRunId() { + return runId; + } - public Dimension getDimension() { - return dimension; - } + public Dimension getDimension() { + return dimension; + } - public Optional getUnit() { - return Optional.ofNullable(unit); - } + public Optional getUnit() { + return Optional.ofNullable(unit); + } - public Optional getInterpretation() { - return Optional.ofNullable(interpretation); - } + public Optional getInterpretation() { + return Optional.ofNullable(interpretation); + } - public Either getContent() { - return content; - } + public Either getContent() { + return content; + } - /** - * Convert this new measurement to an actual {@link Measurement}. - * - * @return the measurement - */ - public Measurement toMeasurement() { - return new Measurement(runId, dimension, content); - } + /** + * Convert this new measurement to an actual {@link Measurement}. + * + * @return the measurement + */ + public Measurement toMeasurement() { + return new Measurement(runId, dimension, content); + } - @Override - public String toString() { - return "NewMeasurement{" + - "runId=" + runId + - ", dimension=" + dimension + - ", unit=" + unit + - ", interpretation=" + interpretation + - ", content=" + content + - '}'; - } + @Override + public String toString() { + return "NewMeasurement{" + + "runId=" + + runId + + ", dimension=" + + dimension + + ", unit=" + + unit + + ", interpretation=" + + interpretation + + ", content=" + + content + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewRun.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewRun.java index dc163fcae..f9a51f264 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewRun.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/NewRun.java @@ -15,122 +15,153 @@ import java.util.Optional; import java.util.UUID; -/** - * An entity used for adding new runs via the BenchmarkAccess. - */ +/** An entity used for adding new runs via the BenchmarkAccess. */ public class NewRun { - private final RunId id; - private final String author; - private final String runnerName; - private final String runnerInfo; - private final Instant startTime; - private final Instant stopTime; - private final Either source; - private final Either> result; - - public NewRun(RunId id, String author, String runnerName, String runnerInfo, Instant startTime, - Instant stopTime, Either source, - RunError error) { - - this(id, author, runnerName, runnerInfo, startTime, stopTime, source, Either.ofLeft(error)); - } - - public NewRun(RunId id, String author, String runnerName, String runnerInfo, Instant startTime, - Instant stopTime, Either source, - Collection measurements) { - - this(id, author, runnerName, runnerInfo, startTime, stopTime, source, - Either.ofRight(measurements)); - } - - private NewRun(RunId id, String author, String runnerName, String runnerInfo, Instant startTime, - Instant stopTime, Either source, - Either> result) { - - this.id = id; - this.author = author; - this.runnerName = runnerName; - this.runnerInfo = runnerInfo; - this.startTime = startTime; - this.stopTime = stopTime; - this.source = source; - this.result = result; - } - - public RunId getId() { - return id; - } - - public UUID getIdAsUuid() { - return id.getId(); - } - - public String getIdAsString() { - return id.getIdAsString(); - } - - public String getAuthor() { - return author; - } - - public String getRunnerName() { - return runnerName; - } - - public String getRunnerInfo() { - return runnerInfo; - } - - public Instant getStartTime() { - return startTime; - } - - public Instant getStopTime() { - return stopTime; - } - - public Either getSource() { - return source; - } - - public Either> getResult() { - return result; - } - - public Optional getRepoId() { - return getSource().consume( - commitSource -> Optional.of(commitSource.getRepoId()), - TarSource::getRepoId - ); - } - - /** - * Convert this new run to an actual {@link Run}. - * - * @return the run - */ - public Run toRun() { - Either> result = this.result.mapRight( - newMeasurements -> newMeasurements.stream() - .map(NewMeasurement::toMeasurement) - .collect(toList()) - ); - - return new Run(id, author, runnerName, runnerInfo, startTime, stopTime, source, result); - } - - @Override - public String toString() { - return "NewRun{" + - "id=" + id + - ", author='" + author + '\'' + - ", runnerName='" + runnerName + '\'' + - ", runnerInfo='" + runnerInfo + '\'' + - ", startTime=" + startTime + - ", stopTime=" + stopTime + - ", source=" + source + - ", result=" + result + - '}'; - } + private final RunId id; + private final String author; + private final String runnerName; + private final String runnerInfo; + private final Instant startTime; + private final Instant stopTime; + private final Either source; + private final Either> result; + + public NewRun( + RunId id, + String author, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + Either source, + RunError error) { + + this(id, author, runnerName, runnerInfo, startTime, stopTime, source, Either.ofLeft(error)); + } + + public NewRun( + RunId id, + String author, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + Either source, + Collection measurements) { + + this( + id, + author, + runnerName, + runnerInfo, + startTime, + stopTime, + source, + Either.ofRight(measurements)); + } + + private NewRun( + RunId id, + String author, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + Either source, + Either> result) { + + this.id = id; + this.author = author; + this.runnerName = runnerName; + this.runnerInfo = runnerInfo; + this.startTime = startTime; + this.stopTime = stopTime; + this.source = source; + this.result = result; + } + + public RunId getId() { + return id; + } + + public UUID getIdAsUuid() { + return id.getId(); + } + + public String getIdAsString() { + return id.getIdAsString(); + } + + public String getAuthor() { + return author; + } + + public String getRunnerName() { + return runnerName; + } + + public String getRunnerInfo() { + return runnerInfo; + } + + public Instant getStartTime() { + return startTime; + } + + public Instant getStopTime() { + return stopTime; + } + + public Either getSource() { + return source; + } + + public Either> getResult() { + return result; + } + + public Optional getRepoId() { + return getSource() + .consume(commitSource -> Optional.of(commitSource.getRepoId()), TarSource::getRepoId); + } + + /** + * Convert this new run to an actual {@link Run}. + * + * @return the run + */ + public Run toRun() { + Either> result = + this.result.mapRight( + newMeasurements -> + newMeasurements.stream().map(NewMeasurement::toMeasurement).collect(toList())); + + return new Run(id, author, runnerName, runnerInfo, startTime, stopTime, source, result); + } + + @Override + public String toString() { + return "NewRun{" + + "id=" + + id + + ", author='" + + author + + '\'' + + ", runnerName='" + + runnerName + + '\'' + + ", runnerInfo='" + + runnerInfo + + '\'' + + ", startTime=" + + startTime + + ", stopTime=" + + stopTime + + ", source=" + + source + + ", result=" + + result + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/RunBuilder.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/RunBuilder.java index bdf616f59..391126e96 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/RunBuilder.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/builder/RunBuilder.java @@ -19,177 +19,175 @@ import java.util.Objects; import javax.annotation.Nullable; -/** - * A basic builder that allows the creation of new runs. - */ +/** A basic builder that allows the creation of new runs. */ public class RunBuilder { - /** - * Creates a new run builder for successful runs. - * - * @param task the task that prompted the run - * @param runnerName the name of the runner that performed the run - * @param runnerInfo some additional info about that runner - * @param startTime the point in time when the runner started the benchmark script - * @param stopTime the point in time when the benchmark script exited - * @return a new builder instance - */ - public static RunBuilder successful(Task task, String runnerName, String runnerInfo, - Instant startTime, Instant stopTime) { - - return new RunBuilder( - new RunId(task.getId().getId()), - task.getAuthor(), - runnerName, - runnerInfo, - startTime, - stopTime, - task.getSource(), - null - ); - } - - /** - * Creates a new run builder for failed runs. - * - * @param task the task that prompted the run - * @param runnerName the name of the runner that performed the run - * @param runnerInfo some additional info about that runner - * @param startTime the start time - * @param stopTime the stop time - * @param errorMessage the error message - * @param runErrorType what kind of error occured - * @return a new builder instance - */ - public static RunBuilder failed(Task task, String runnerName, String runnerInfo, - Instant startTime, Instant stopTime, String errorMessage, RunErrorType runErrorType) { - - return new RunBuilder( - new RunId(task.getId().getId()), - task.getAuthor(), - runnerName, - runnerInfo, - startTime, - stopTime, - task.getSource(), - new RunError(errorMessage, runErrorType) - ); - } - - private final RunId runId; - private final String author; - private final String runnerName; - private final String runnerInfo; - private final Instant startTime; - private final Instant stopTime; - @Nullable - private final Either source; - private final List measurementList; - @Nullable - private final RunError error; - - private RunBuilder(RunId runId, String author, String runnerName, String runnerInfo, - Instant startTime, Instant stopTime, - @Nullable Either source, @Nullable RunError error) { - - this.runId = runId; - this.author = Objects.requireNonNull(author); - this.runnerName = Objects.requireNonNull(runnerName); - this.runnerInfo = Objects.requireNonNull(runnerInfo); - this.startTime = Objects.requireNonNull(startTime); - this.stopTime = Objects.requireNonNull(stopTime); - this.source = source; - this.error = error; - this.measurementList = error != null ? Collections.emptyList() : new ArrayList<>(); - } - - /** - * Adds a new successful measurement to the run. - * - * @param name the name of the measurement - * @param interpretation the interpretation - * @param unit the unit - * @param values the values - */ - public void addSuccessfulMeasurement(Dimension name, Unit unit, Interpretation interpretation, - List values) { - - MeasurementValues measurementValues = new MeasurementValues(values); - NewMeasurement measurement = new NewMeasurement( - this.runId, name, unit, interpretation, measurementValues - ); - - this.measurementList.add(measurement); - } - - /** - * Adds a new failed measurement to the run. - * - * @param name the name of the measurement - * @param unit the unit of the measurement - * @param interpretation how the measurement is to be interpreted - * @param errorMessage the error message - */ - public void addFailedMeasurement(Dimension name, Unit unit, Interpretation interpretation, - String errorMessage) { - - MeasurementError measurementError = new MeasurementError(errorMessage); - NewMeasurement measurement = new NewMeasurement( - this.runId, name, unit, interpretation, measurementError - ); - - this.measurementList.add(measurement); - } - - /** - * Builds the specified run instance. - * - * @return the created run instance - * @throws IllegalStateException if this run cannot be built yet. - */ - public NewRun build() throws IllegalStateException { - if (error != null) { - return new NewRun( - runId, - author, - runnerName, - runnerInfo, - startTime, - stopTime, - source, - error - ); - } else { - if (measurementList.isEmpty()) { - throw new IllegalStateException("measurement list is empty for: " + this); - } - - return new NewRun( - runId, - author, - runnerName, - runnerInfo, - startTime, - stopTime, - source, - measurementList - ); - } - } - - @Override - public String toString() { - return "RunBuilder{" + - "runId=" + runId + - ", author='" + author + '\'' + - ", runnerName='" + runnerName + '\'' + - ", runnerInfo='" + runnerInfo + '\'' + - ", startTime=" + startTime + - ", stopTime=" + stopTime + - ", source=" + source + - ", measurementList=" + measurementList + - ", error=" + error + - '}'; - } - + /** + * Creates a new run builder for successful runs. + * + * @param task the task that prompted the run + * @param runnerName the name of the runner that performed the run + * @param runnerInfo some additional info about that runner + * @param startTime the point in time when the runner started the benchmark script + * @param stopTime the point in time when the benchmark script exited + * @return a new builder instance + */ + public static RunBuilder successful( + Task task, String runnerName, String runnerInfo, Instant startTime, Instant stopTime) { + + return new RunBuilder( + new RunId(task.getId().getId()), + task.getAuthor(), + runnerName, + runnerInfo, + startTime, + stopTime, + task.getSource(), + null); + } + + /** + * Creates a new run builder for failed runs. + * + * @param task the task that prompted the run + * @param runnerName the name of the runner that performed the run + * @param runnerInfo some additional info about that runner + * @param startTime the start time + * @param stopTime the stop time + * @param errorMessage the error message + * @param runErrorType what kind of error occured + * @return a new builder instance + */ + public static RunBuilder failed( + Task task, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + String errorMessage, + RunErrorType runErrorType) { + + return new RunBuilder( + new RunId(task.getId().getId()), + task.getAuthor(), + runnerName, + runnerInfo, + startTime, + stopTime, + task.getSource(), + new RunError(errorMessage, runErrorType)); + } + + private final RunId runId; + private final String author; + private final String runnerName; + private final String runnerInfo; + private final Instant startTime; + private final Instant stopTime; + @Nullable private final Either source; + private final List measurementList; + @Nullable private final RunError error; + + private RunBuilder( + RunId runId, + String author, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + @Nullable Either source, + @Nullable RunError error) { + + this.runId = runId; + this.author = Objects.requireNonNull(author); + this.runnerName = Objects.requireNonNull(runnerName); + this.runnerInfo = Objects.requireNonNull(runnerInfo); + this.startTime = Objects.requireNonNull(startTime); + this.stopTime = Objects.requireNonNull(stopTime); + this.source = source; + this.error = error; + this.measurementList = error != null ? Collections.emptyList() : new ArrayList<>(); + } + + /** + * Adds a new successful measurement to the run. + * + * @param name the name of the measurement + * @param interpretation the interpretation + * @param unit the unit + * @param values the values + */ + public void addSuccessfulMeasurement( + Dimension name, Unit unit, Interpretation interpretation, List values) { + + MeasurementValues measurementValues = new MeasurementValues(values); + NewMeasurement measurement = + new NewMeasurement(this.runId, name, unit, interpretation, measurementValues); + + this.measurementList.add(measurement); + } + + /** + * Adds a new failed measurement to the run. + * + * @param name the name of the measurement + * @param unit the unit of the measurement + * @param interpretation how the measurement is to be interpreted + * @param errorMessage the error message + */ + public void addFailedMeasurement( + Dimension name, Unit unit, Interpretation interpretation, String errorMessage) { + + MeasurementError measurementError = new MeasurementError(errorMessage); + NewMeasurement measurement = + new NewMeasurement(this.runId, name, unit, interpretation, measurementError); + + this.measurementList.add(measurement); + } + + /** + * Builds the specified run instance. + * + * @return the created run instance + * @throws IllegalStateException if this run cannot be built yet. + */ + public NewRun build() throws IllegalStateException { + if (error != null) { + return new NewRun(runId, author, runnerName, runnerInfo, startTime, stopTime, source, error); + } else { + if (measurementList.isEmpty()) { + throw new IllegalStateException("measurement list is empty for: " + this); + } + + return new NewRun( + runId, author, runnerName, runnerInfo, startTime, stopTime, source, measurementList); + } + } + + @Override + public String toString() { + return "RunBuilder{" + + "runId=" + + runId + + ", author='" + + author + + '\'' + + ", runnerName='" + + runnerName + + '\'' + + ", runnerInfo='" + + runnerInfo + + '\'' + + ", startTime=" + + startTime + + ", stopTime=" + + stopTime + + ", source=" + + source + + ", measurementList=" + + measurementList + + ", error=" + + error + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Measurement.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Measurement.java index 11b0dce83..f36c7f623 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Measurement.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Measurement.java @@ -11,45 +11,46 @@ */ public class Measurement { - private final RunId runId; - private final Dimension dimension; - private final Either content; - - public Measurement(RunId runId, Dimension dimension, - Either content) { - - this.runId = runId; - this.dimension = dimension; - this.content = content; - } - - public RunId getRunId() { - return runId; - } - - public Dimension getDimension() { - return dimension; - } - - public Either getContent() { - return content; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Measurement that = (Measurement) o; - return Objects.equals(runId, that.runId) && Objects - .equals(dimension, that.dimension) && Objects.equals(content, that.content); - } - - @Override - public int hashCode() { - return Objects.hash(runId, dimension, content); - } + private final RunId runId; + private final Dimension dimension; + private final Either content; + + public Measurement( + RunId runId, Dimension dimension, Either content) { + + this.runId = runId; + this.dimension = dimension; + this.content = content; + } + + public RunId getRunId() { + return runId; + } + + public Dimension getDimension() { + return dimension; + } + + public Either getContent() { + return content; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Measurement that = (Measurement) o; + return Objects.equals(runId, that.runId) + && Objects.equals(dimension, that.dimension) + && Objects.equals(content, that.content); + } + + @Override + public int hashCode() { + return Objects.hash(runId, dimension, content); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementError.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementError.java index 3b0800786..3c20cbde7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementError.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementError.java @@ -2,43 +2,38 @@ import java.util.Objects; -/** - * This class represents a failed {@link Measurement}'s state, which only contains an message. - */ +/** This class represents a failed {@link Measurement}'s state, which only contains an message. */ public class MeasurementError { - private final String errorMessage; - - public MeasurementError(String errorMessage) { - this.errorMessage = Objects.requireNonNull(errorMessage); - } - - public String getErrorMessage() { - return errorMessage; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - MeasurementError that = (MeasurementError) o; - return errorMessage.equals(that.errorMessage); - } - - @Override - public int hashCode() { - return Objects.hash(errorMessage); - } - - @Override - public String toString() { - return "MeasurementError{" + - "errorMessage='" + errorMessage + '\'' + - '}'; - } - + private final String errorMessage; + + public MeasurementError(String errorMessage) { + this.errorMessage = Objects.requireNonNull(errorMessage); + } + + public String getErrorMessage() { + return errorMessage; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MeasurementError that = (MeasurementError) o; + return errorMessage.equals(that.errorMessage); + } + + @Override + public int hashCode() { + return Objects.hash(errorMessage); + } + + @Override + public String toString() { + return "MeasurementError{" + "errorMessage='" + errorMessage + '\'' + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementValues.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementValues.java index 1f07c2bac..04b1ef17c 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementValues.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/MeasurementValues.java @@ -14,102 +14,97 @@ */ public class MeasurementValues { - /** - * Calculates the mean of the given values. - * - * @param values the values - * @return the mean - */ - public static double calculateAverage(List values) { - double sum = 0; - for (Double value : values) { - sum += value; - } - - return sum / values.size(); - } - - private final List values; - - public MeasurementValues(List values) { - this.values = Collections.unmodifiableList(Objects.requireNonNull(values)); - if (this.values.isEmpty()) { - throw new IllegalArgumentException("list of values must not be empty"); - } - } - - public List getValues() { - return values; - } - - public double getAverageValue() { - return calculateAverage(values); - } - - /** - * @param significanceFactors the significance factors to use - * @return the standard deviation of the values, if there are more than {@link - * SignificanceFactors#getMinStddevAmount()} values - */ - public Optional getStddevWith(SignificanceFactors significanceFactors) { - if (values.size() >= significanceFactors.getMinStddevAmount()) { - return getStddev(); - } else { - return Optional.empty(); - } - } - - /** - * @return the standard deviation of the values, if there are at least two values. WARNING: When - * calling this function, always use {@link SignificanceFactors#getMinStddevAmount()} to see - * whether a standard deviation even makes sense in this situation. - */ - private Optional getStddev() { - int n = values.size(); - if (n < 2) { - return Optional.empty(); - } - - double avg = getAverageValue(); - double sum = values.stream() - .map(value -> (value - avg) * (value - avg)) - .reduce(0.0, Double::sum); - double result = Math.sqrt(sum / (n - 1)); - return Optional.of(result); - } - - /** - * @param significanceFactors the significance factors to use - * @return stddev / averageValue, if there are more than {@link SignificanceFactors#getMinStddevAmount()} - * values - */ - public Optional getStddevPercentWith(SignificanceFactors significanceFactors) { - return getStddevWith(significanceFactors) - .map(it -> it / getAverageValue()); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - MeasurementValues that = (MeasurementValues) o; - return values.equals(that.values); - } - - @Override - public int hashCode() { - return Objects.hash(values); - } - - @Override - public String toString() { - return "MeasurementValues{" + - "values=" + values + - '}'; - } - + /** + * Calculates the mean of the given values. + * + * @param values the values + * @return the mean + */ + public static double calculateAverage(List values) { + double sum = 0; + for (Double value : values) { + sum += value; + } + + return sum / values.size(); + } + + private final List values; + + public MeasurementValues(List values) { + this.values = Collections.unmodifiableList(Objects.requireNonNull(values)); + if (this.values.isEmpty()) { + throw new IllegalArgumentException("list of values must not be empty"); + } + } + + public List getValues() { + return values; + } + + public double getAverageValue() { + return calculateAverage(values); + } + + /** + * @param significanceFactors the significance factors to use + * @return the standard deviation of the values, if there are more than {@link + * SignificanceFactors#getMinStddevAmount()} values + */ + public Optional getStddevWith(SignificanceFactors significanceFactors) { + if (values.size() >= significanceFactors.getMinStddevAmount()) { + return getStddev(); + } else { + return Optional.empty(); + } + } + + /** + * @return the standard deviation of the values, if there are at least two values. WARNING: When + * calling this function, always use {@link SignificanceFactors#getMinStddevAmount()} to see + * whether a standard deviation even makes sense in this situation. + */ + private Optional getStddev() { + int n = values.size(); + if (n < 2) { + return Optional.empty(); + } + + double avg = getAverageValue(); + double sum = + values.stream().map(value -> (value - avg) * (value - avg)).reduce(0.0, Double::sum); + double result = Math.sqrt(sum / (n - 1)); + return Optional.of(result); + } + + /** + * @param significanceFactors the significance factors to use + * @return stddev / averageValue, if there are more than {@link + * SignificanceFactors#getMinStddevAmount()} values + */ + public Optional getStddevPercentWith(SignificanceFactors significanceFactors) { + return getStddevWith(significanceFactors).map(it -> it / getAverageValue()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MeasurementValues that = (MeasurementValues) o; + return values.equals(that.values); + } + + @Override + public int hashCode() { + return Objects.hash(values); + } + + @Override + public String toString() { + return "MeasurementValues{" + "values=" + values + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Run.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Run.java index 8c2999b2f..1f71f8234 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Run.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/Run.java @@ -15,94 +15,108 @@ /** * A run is a single execution of the benchmark script. * - *

It can either be successful, in which case it may contain multiple successful and failed + *

It can either be successful, in which case it may contain multiple successful and failed * {@link Measurement}s, or it can be failed, in which case it only contains an error message and no * {@link Measurement}s. Specifically, a successful {@link Run} can still contain failed {@link * Measurement}s. */ public class Run { - private final RunId id; - private final String author; - private final String runnerName; - private final String runnerInfo; - private final Instant startTime; - private final Instant stopTime; - private final Either source; - private final Either> result; - - public Run(RunId id, String author, String runnerName, String runnerInfo, Instant startTime, - Instant stopTime, Either source, - Either> result) { - - this.id = Objects.requireNonNull(id); - this.author = Objects.requireNonNull(author); - this.runnerName = Objects.requireNonNull(runnerName); - this.runnerInfo = Objects.requireNonNull(runnerInfo); - this.startTime = Objects.requireNonNull(startTime); - this.stopTime = Objects.requireNonNull(stopTime); - this.source = Objects.requireNonNull(source); - this.result = result; - } - - public RunId getId() { - return id; - } - - public String getAuthor() { - return author; - } - - public String getRunnerName() { - return runnerName; - } - - public String getRunnerInfo() { - return runnerInfo; - } - - public Either getSource() { - return source; - } - - public Instant getStartTime() { - return startTime; - } - - public Instant getStopTime() { - return stopTime; - } - - public Either> getResult() { - return result; - } - - public Optional getRepoId() { - return getSource().consume( - commitSource -> Optional.of(commitSource.getRepoId()), - TarSource::getRepoId - ); - } - - public Set getAllDimensionsUsed() { - return result.getRight().stream() - .flatMap(Collection::stream) - .map(Measurement::getDimension) - .collect(Collectors.toSet()); - } - - @Override - public String toString() { - return "Run{" + - "id=" + id + - ", author='" + author + '\'' + - ", runnerName='" + runnerName + '\'' + - ", runnerInfo='" + runnerInfo + '\'' + - ", startTime=" + startTime + - ", stopTime=" + stopTime + - ", source=" + source + - ", result=" + result + - '}'; - } + private final RunId id; + private final String author; + private final String runnerName; + private final String runnerInfo; + private final Instant startTime; + private final Instant stopTime; + private final Either source; + private final Either> result; + + public Run( + RunId id, + String author, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + Either source, + Either> result) { + + this.id = Objects.requireNonNull(id); + this.author = Objects.requireNonNull(author); + this.runnerName = Objects.requireNonNull(runnerName); + this.runnerInfo = Objects.requireNonNull(runnerInfo); + this.startTime = Objects.requireNonNull(startTime); + this.stopTime = Objects.requireNonNull(stopTime); + this.source = Objects.requireNonNull(source); + this.result = result; + } + + public RunId getId() { + return id; + } + + public String getAuthor() { + return author; + } + + public String getRunnerName() { + return runnerName; + } + + public String getRunnerInfo() { + return runnerInfo; + } + + public Either getSource() { + return source; + } + + public Instant getStartTime() { + return startTime; + } + + public Instant getStopTime() { + return stopTime; + } + + public Either> getResult() { + return result; + } + + public Optional getRepoId() { + return getSource() + .consume(commitSource -> Optional.of(commitSource.getRepoId()), TarSource::getRepoId); + } + + public Set getAllDimensionsUsed() { + return result.getRight().stream() + .flatMap(Collection::stream) + .map(Measurement::getDimension) + .collect(Collectors.toSet()); + } + + @Override + public String toString() { + return "Run{" + + "id=" + + id + + ", author='" + + author + + '\'' + + ", runnerName='" + + runnerName + + '\'' + + ", runnerInfo='" + + runnerInfo + + '\'' + + ", startTime=" + + startTime + + ", stopTime=" + + stopTime + + ", source=" + + source + + ", result=" + + result + + '}'; + } } - diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunError.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunError.java index 6b1ec85c2..cfc1ec1a5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunError.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunError.java @@ -8,45 +8,41 @@ */ public class RunError { - private final String errorMessage; - private final RunErrorType type; - - public RunError(String errorMessage, RunErrorType type) { - this.errorMessage = Objects.requireNonNull(errorMessage); - this.type = Objects.requireNonNull(type); - } - - public String getMessage() { - return errorMessage; - } - - public RunErrorType getType() { - return type; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RunError runError = (RunError) o; - return Objects.equals(errorMessage, runError.errorMessage) && type == runError.type; - } - - @Override - public int hashCode() { - return Objects.hash(errorMessage, type); - } - - @Override - public String toString() { - return "RunError{" + - "errorMessage='" + errorMessage + '\'' + - ", type=" + type + - '}'; - } - + private final String errorMessage; + private final RunErrorType type; + + public RunError(String errorMessage, RunErrorType type) { + this.errorMessage = Objects.requireNonNull(errorMessage); + this.type = Objects.requireNonNull(type); + } + + public String getMessage() { + return errorMessage; + } + + public RunErrorType getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RunError runError = (RunError) o; + return Objects.equals(errorMessage, runError.errorMessage) && type == runError.type; + } + + @Override + public int hashCode() { + return Objects.hash(errorMessage, type); + } + + @Override + public String toString() { + return "RunError{" + "errorMessage='" + errorMessage + '\'' + ", type=" + type + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunErrorType.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunErrorType.java index 142e3d00d..b9e44dae7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunErrorType.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunErrorType.java @@ -1,47 +1,40 @@ package de.aaaaaaah.velcom.backend.access.benchmarkaccess.entities; -/** - * Describes which type of error has occurred when trying to perform a benchmark run. - */ +/** Describes which type of error has occurred when trying to perform a benchmark run. */ public enum RunErrorType { - /** - * The benchmark script returned a global error. - */ - BENCH_SCRIPT_ERROR("BENCH"), - - /** - * Some error occurred in velcom which prevented the execution of the benchmark script. - */ - VELCOM_ERROR("VELCOM"); - - private final String textualRepresentation; - - RunErrorType(String textualRepresentation) { - this.textualRepresentation = textualRepresentation; - } - - public String getTextualRepresentation() { - return textualRepresentation; - } - - /** - * Tries to find the error type that matches the given string representation. - * - * @param representation the representation - * @return the error type that matches the given string representation - * @throws IllegalArgumentException if no error type matches the given string representation - */ - public static RunErrorType fromTextualRepresentation(String representation) - throws IllegalArgumentException { - - for (RunErrorType runErrorType : RunErrorType.values()) { - if (runErrorType.getTextualRepresentation().equals(representation)) { - return runErrorType; - } - } - - throw new IllegalArgumentException("\"" + representation + "\" is not a valid error type"); - } + /** The benchmark script returned a global error. */ + BENCH_SCRIPT_ERROR("BENCH"), + /** Some error occurred in velcom which prevented the execution of the benchmark script. */ + VELCOM_ERROR("VELCOM"); + + private final String textualRepresentation; + + RunErrorType(String textualRepresentation) { + this.textualRepresentation = textualRepresentation; + } + + public String getTextualRepresentation() { + return textualRepresentation; + } + + /** + * Tries to find the error type that matches the given string representation. + * + * @param representation the representation + * @return the error type that matches the given string representation + * @throws IllegalArgumentException if no error type matches the given string representation + */ + public static RunErrorType fromTextualRepresentation(String representation) + throws IllegalArgumentException { + + for (RunErrorType runErrorType : RunErrorType.values()) { + if (runErrorType.getTextualRepresentation().equals(representation)) { + return runErrorType; + } + } + + throw new IllegalArgumentException("\"" + representation + "\" is not a valid error type"); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunId.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunId.java index 53c32aca8..3ece1cfe9 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunId.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/RunId.java @@ -4,73 +4,66 @@ import java.util.Objects; import java.util.UUID; -/** - * A unique identifier for a {@link Run}. - */ +/** A unique identifier for a {@link Run}. */ public class RunId { - private final UUID id; + private final UUID id; - public RunId(UUID id) { - this.id = Objects.requireNonNull(id); - } + public RunId(UUID id) { + this.id = Objects.requireNonNull(id); + } - /** - * Create a new, random {@link RunId}. - */ - public RunId() { - this(UUID.randomUUID()); - } + /** Create a new, random {@link RunId}. */ + public RunId() { + this(UUID.randomUUID()); + } - /** - * Create a new {@link RunId} from a UUID string. - * - * @param string the UUID as string - * @return the run id - */ - public static RunId fromString(String string) { - return new RunId(UUID.fromString(string)); - } + /** + * Create a new {@link RunId} from a UUID string. + * + * @param string the UUID as string + * @return the run id + */ + public static RunId fromString(String string) { + return new RunId(UUID.fromString(string)); + } - public UUID getId() { - return id; - } + public UUID getId() { + return id; + } - public String getIdAsString() { - return id.toString(); - } + public String getIdAsString() { + return id.toString(); + } - /** - * Convert a run id to a task id. - * - * @return a task id with the same UUID - */ - public TaskId toTaskId() { - return new TaskId(id); - } + /** + * Convert a run id to a task id. + * + * @return a task id with the same UUID + */ + public TaskId toTaskId() { + return new TaskId(id); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RunId runId = (RunId) o; - return Objects.equals(id, runId.id); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RunId runId = (RunId) o; + return Objects.equals(id, runId.id); + } - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return "RunId{" + - "id=" + id + - '}'; - } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString() { + return "RunId{" + "id=" + id + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/SearchRunDescription.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/SearchRunDescription.java index 128ea5fac..87cb3e61b 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/SearchRunDescription.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/SearchRunDescription.java @@ -7,68 +7,78 @@ public class SearchRunDescription { - private final RunId id; - @Nullable - private final RepoId repoId; - @Nullable - private final String commitSummary; - @Nullable - private final String tarDescription; - private final Instant startTime; - private final Instant stopTime; + private final RunId id; + @Nullable private final RepoId repoId; + @Nullable private final String commitSummary; + @Nullable private final String tarDescription; + private final Instant startTime; + private final Instant stopTime; - public SearchRunDescription(RunId id, @Nullable RepoId repoId, @Nullable String commitSummary, - @Nullable String tarDescription, Instant startTime, Instant stopTime) { + public SearchRunDescription( + RunId id, + @Nullable RepoId repoId, + @Nullable String commitSummary, + @Nullable String tarDescription, + Instant startTime, + Instant stopTime) { - if (commitSummary != null && repoId == null) { - throw new IllegalArgumentException("repoId must not be null if commitSummary exists"); - } - if ((commitSummary == null) == (tarDescription == null)) { - throw new IllegalArgumentException( - "exactly one of commitSummary and tarDescription must be null"); - } + if (commitSummary != null && repoId == null) { + throw new IllegalArgumentException("repoId must not be null if commitSummary exists"); + } + if ((commitSummary == null) == (tarDescription == null)) { + throw new IllegalArgumentException( + "exactly one of commitSummary and tarDescription must be null"); + } - this.id = id; - this.repoId = repoId; - this.commitSummary = commitSummary; - this.tarDescription = tarDescription; - this.startTime = startTime; - this.stopTime = stopTime; - } + this.id = id; + this.repoId = repoId; + this.commitSummary = commitSummary; + this.tarDescription = tarDescription; + this.startTime = startTime; + this.stopTime = stopTime; + } - public RunId getId() { - return id; - } + public RunId getId() { + return id; + } - public Optional getRepoId() { - return Optional.ofNullable(repoId); - } + public Optional getRepoId() { + return Optional.ofNullable(repoId); + } - public Optional getCommitSummary() { - return Optional.ofNullable(commitSummary); - } + public Optional getCommitSummary() { + return Optional.ofNullable(commitSummary); + } - public Optional getTarDescription() { - return Optional.ofNullable(tarDescription); - } + public Optional getTarDescription() { + return Optional.ofNullable(tarDescription); + } - public Instant getStartTime() { - return startTime; - } + public Instant getStartTime() { + return startTime; + } - public Instant getStopTime() { - return stopTime; - } + public Instant getStopTime() { + return stopTime; + } - @Override - public String toString() { - return "SearchRunDescription{" + - "id=" + id + - ", repoId=" + repoId + - ", commitSummary='" + commitSummary + '\'' + - ", tarDescription='" + tarDescription + '\'' + - ", startTime=" + startTime + - ", stopTime=" + stopTime + - '}'; - } + @Override + public String toString() { + return "SearchRunDescription{" + + "id=" + + id + + ", repoId=" + + repoId + + ", commitSummary='" + + commitSummary + + '\'' + + ", tarDescription='" + + tarDescription + + '\'' + + ", startTime=" + + startTime + + ", stopTime=" + + stopTime + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/ShortRunDescription.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/ShortRunDescription.java index 75d600b28..c5b7790b3 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/ShortRunDescription.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/ShortRunDescription.java @@ -8,82 +8,88 @@ public class ShortRunDescription { - private final RunId id; + private final RunId id; - // Not using an Either since this data type's abstraction level is pretty low and in the places it - // is used, we don't really need the guarantees that would give us. - @Nullable - private final String commitHash; - @Nullable - private final String commitSummary; - @Nullable - private final String tarDescription; + // Not using an Either since this data type's abstraction level is pretty low and in the places it + // is used, we don't really need the guarantees that would give us. + @Nullable private final String commitHash; + @Nullable private final String commitSummary; + @Nullable private final String tarDescription; - public ShortRunDescription(RunId id, @Nullable String commitHash, @Nullable String commitMessage, - @Nullable String tarDescription) { + public ShortRunDescription( + RunId id, + @Nullable String commitHash, + @Nullable String commitMessage, + @Nullable String tarDescription) { - boolean commitHashNull = commitHash == null; - boolean commitMessageNull = commitMessage == null; - boolean tarDescriptionNull = tarDescription == null; - if (commitHashNull != commitMessageNull) { - throw new IllegalArgumentException( - "commitHash and commitMessage must either both be null or non-null"); - } - if (commitHashNull == tarDescriptionNull) { - throw new IllegalArgumentException( - "exactly one of commitHash and tarDescription must be null"); - } + boolean commitHashNull = commitHash == null; + boolean commitMessageNull = commitMessage == null; + boolean tarDescriptionNull = tarDescription == null; + if (commitHashNull != commitMessageNull) { + throw new IllegalArgumentException( + "commitHash and commitMessage must either both be null or non-null"); + } + if (commitHashNull == tarDescriptionNull) { + throw new IllegalArgumentException( + "exactly one of commitHash and tarDescription must be null"); + } - this.id = id; - this.commitHash = commitHash; - this.commitSummary = Optional.ofNullable(commitMessage) - .map(Commit::splitMessageIntoSections) - .map(Pair::getFirst) - .orElse(null); - this.tarDescription = tarDescription; - } + this.id = id; + this.commitHash = commitHash; + this.commitSummary = + Optional.ofNullable(commitMessage) + .map(Commit::splitMessageIntoSections) + .map(Pair::getFirst) + .orElse(null); + this.tarDescription = tarDescription; + } - public RunId getId() { - return id; - } + public RunId getId() { + return id; + } - public Optional getCommitHash() { - return Optional.ofNullable(commitHash); - } + public Optional getCommitHash() { + return Optional.ofNullable(commitHash); + } - public Optional getCommitSummary() { - return Optional.ofNullable(commitSummary); - } + public Optional getCommitSummary() { + return Optional.ofNullable(commitSummary); + } - public Optional getTarDescription() { - return Optional.ofNullable(tarDescription); - } + public Optional getTarDescription() { + return Optional.ofNullable(tarDescription); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ShortRunDescription that = (ShortRunDescription) o; - return Objects.equals(id, that.id) && Objects - .equals(commitSummary, that.commitSummary) && Objects - .equals(tarDescription, that.tarDescription); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ShortRunDescription that = (ShortRunDescription) o; + return Objects.equals(id, that.id) + && Objects.equals(commitSummary, that.commitSummary) + && Objects.equals(tarDescription, that.tarDescription); + } - @Override - public int hashCode() { - return Objects.hash(id, commitSummary, tarDescription); - } + @Override + public int hashCode() { + return Objects.hash(id, commitSummary, tarDescription); + } - @Override - public String toString() { - return "ShortRunDescription{" + - "id=" + id + - ", commitSummary='" + commitSummary + '\'' + - ", tarDescription='" + tarDescription + '\'' + - '}'; - } + @Override + public String toString() { + return "ShortRunDescription{" + + "id=" + + id + + ", commitSummary='" + + commitSummary + + '\'' + + ", tarDescription='" + + tarDescription + + '\'' + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/CommitSource.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/CommitSource.java index a21edf250..e3a5b52f0 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/CommitSource.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/CommitSource.java @@ -5,60 +5,54 @@ import de.aaaaaaah.velcom.backend.access.repoaccess.entities.RepoId; import java.util.Objects; -/** - * A source describing that the task originated from a commit in a repository. - */ +/** A source describing that the task originated from a commit in a repository. */ public class CommitSource { - private final RepoId repoId; - private final CommitHash hash; - - public CommitSource(RepoId repoId, CommitHash hash) { - this.repoId = Objects.requireNonNull(repoId); - this.hash = Objects.requireNonNull(hash); - } - - /** - * Create a new {@link CommitSource} from an existing commit. - * - * @param commit the commit to create the source from - * @return the source - */ - public static CommitSource fromCommit(Commit commit) { - return new CommitSource(commit.getRepoId(), commit.getHash()); - } - - public RepoId getRepoId() { - return repoId; - } - - public CommitHash getHash() { - return hash; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CommitSource that = (CommitSource) o; - return Objects.equals(repoId, that.repoId) && Objects.equals(hash, that.hash); - } - - @Override - public int hashCode() { - return Objects.hash(repoId, hash); - } - - @Override - public String toString() { - return "CommitSource{" + - "repoId=" + repoId + - ", hash=" + hash + - '}'; - } - + private final RepoId repoId; + private final CommitHash hash; + + public CommitSource(RepoId repoId, CommitHash hash) { + this.repoId = Objects.requireNonNull(repoId); + this.hash = Objects.requireNonNull(hash); + } + + /** + * Create a new {@link CommitSource} from an existing commit. + * + * @param commit the commit to create the source from + * @return the source + */ + public static CommitSource fromCommit(Commit commit) { + return new CommitSource(commit.getRepoId(), commit.getHash()); + } + + public RepoId getRepoId() { + return repoId; + } + + public CommitHash getHash() { + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CommitSource that = (CommitSource) o; + return Objects.equals(repoId, that.repoId) && Objects.equals(hash, that.hash); + } + + @Override + public int hashCode() { + return Objects.hash(repoId, hash); + } + + @Override + public String toString() { + return "CommitSource{" + "repoId=" + repoId + ", hash=" + hash + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/TarSource.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/TarSource.java index 085ebfe75..ec3046e88 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/TarSource.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/entities/sources/TarSource.java @@ -5,52 +5,45 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * A source describing that the task originated from a tar. The task may be attached to a repo. - */ +/** A source describing that the task originated from a tar. The task may be attached to a repo. */ public class TarSource { - private final String description; - @Nullable - private final RepoId repoId; - - public TarSource(String description, @Nullable RepoId repoId) { - this.description = Objects.requireNonNull(description); - this.repoId = repoId; - } - - public String getDescription() { - return description; - } - - public Optional getRepoId() { - return Optional.ofNullable(repoId); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TarSource tarSource = (TarSource) o; - return Objects.equals(description, tarSource.description) && Objects - .equals(repoId, tarSource.repoId); - } - - @Override - public int hashCode() { - return Objects.hash(description, repoId); - } - - @Override - public String toString() { - return "TarSource{" + - "description='" + description + '\'' + - ", repoId=" + repoId + - '}'; - } - + private final String description; + @Nullable private final RepoId repoId; + + public TarSource(String description, @Nullable RepoId repoId) { + this.description = Objects.requireNonNull(description); + this.repoId = repoId; + } + + public String getDescription() { + return description; + } + + public Optional getRepoId() { + return Optional.ofNullable(repoId); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TarSource tarSource = (TarSource) o; + return Objects.equals(description, tarSource.description) + && Objects.equals(repoId, tarSource.repoId); + } + + @Override + public int hashCode() { + return Objects.hash(description, repoId); + } + + @Override + public String toString() { + return "TarSource{" + "description='" + description + '\'' + ", repoId=" + repoId + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/exceptions/NoSuchRunException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/exceptions/NoSuchRunException.java index 07cd8612f..0abf95c43 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/exceptions/NoSuchRunException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/exceptions/NoSuchRunException.java @@ -6,24 +6,22 @@ import de.aaaaaaah.velcom.shared.util.Either; import de.aaaaaaah.velcom.shared.util.Pair; -/** - * This exception is thrown whenever an invalid {@link RunId} is used. - */ +/** This exception is thrown whenever an invalid {@link RunId} is used. */ public class NoSuchRunException extends RuntimeException { - private final Either> invalidSource; + private final Either> invalidSource; - public NoSuchRunException(RunId invalidId) { - super("no run with id " + invalidId); - invalidSource = Either.ofLeft(invalidId); - } + public NoSuchRunException(RunId invalidId) { + super("no run with id " + invalidId); + invalidSource = Either.ofLeft(invalidId); + } - public NoSuchRunException(RepoId invalidRepoId, CommitHash invalidCommitHash) { - super("no run for commit " + invalidCommitHash + " in repo " + invalidRepoId); - invalidSource = Either.ofRight(new Pair<>(invalidRepoId, invalidCommitHash)); - } + public NoSuchRunException(RepoId invalidRepoId, CommitHash invalidCommitHash) { + super("no run for commit " + invalidCommitHash + " in repo " + invalidRepoId); + invalidSource = Either.ofRight(new Pair<>(invalidRepoId, invalidCommitHash)); + } - public Either> getInvalidSource() { - return invalidSource; - } + public Either> getInvalidSource() { + return invalidSource; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/AvailableDimensionsCache.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/AvailableDimensionsCache.java index 8cb0a20b7..2b6d712b7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/AvailableDimensionsCache.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/AvailableDimensionsCache.java @@ -17,48 +17,43 @@ */ public class AvailableDimensionsCache { - private static final int MAXIMUM_SIZE = 500; - - private final Cache> cache; - - public AvailableDimensionsCache() { - cache = Caffeine.newBuilder() - .maximumSize(MAXIMUM_SIZE) - .build(); - } - - public Map> getAvailableDimensions(DimensionReadAccess dimensionAccess, - Collection repoIds) { - - return cache.getAll( - repoIds, - missingIdsIt -> { - List missingIds = new ArrayList<>(); - missingIdsIt.forEach(missingIds::add); - return dimensionAccess.getAvailableDimensions(missingIds); - } - ); - } - - public Set getAvailableDimensionsFor(DimensionReadAccess dimensionReadAccess, - RepoId repoId) { - - return cache.get(repoId, dimensionReadAccess::getAvailableDimensions); - } - - /** - * Invalidate a specific repo's entry. - * - * @param repoId the id of the repo whose dimensions to remove from the cache - */ - public void invalidate(RepoId repoId) { - cache.invalidate(repoId); - } - - /** - * Invalidate all entries. - */ - public void invalidateAll() { - cache.invalidateAll(); - } + private static final int MAXIMUM_SIZE = 500; + + private final Cache> cache; + + public AvailableDimensionsCache() { + cache = Caffeine.newBuilder().maximumSize(MAXIMUM_SIZE).build(); + } + + public Map> getAvailableDimensions( + DimensionReadAccess dimensionAccess, Collection repoIds) { + + return cache.getAll( + repoIds, + missingIdsIt -> { + List missingIds = new ArrayList<>(); + missingIdsIt.forEach(missingIds::add); + return dimensionAccess.getAvailableDimensions(missingIds); + }); + } + + public Set getAvailableDimensionsFor( + DimensionReadAccess dimensionReadAccess, RepoId repoId) { + + return cache.get(repoId, dimensionReadAccess::getAvailableDimensions); + } + + /** + * Invalidate a specific repo's entry. + * + * @param repoId the id of the repo whose dimensions to remove from the cache + */ + public void invalidate(RepoId repoId) { + cache.invalidate(repoId); + } + + /** Invalidate all entries. */ + public void invalidateAll() { + cache.invalidateAll(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/LatestRunCache.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/LatestRunCache.java index 34a2fd5bd..6b9e3f078 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/LatestRunCache.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/LatestRunCache.java @@ -23,91 +23,85 @@ */ public class LatestRunCache { - private static final int MAXIMUM_SIZE = 10000; - - private final ConcurrentHashMap>> cache; - - public LatestRunCache() { - cache = new ConcurrentHashMap<>(); - } - - private Cache> getCacheForRepo(RepoId repoId) { - return cache.computeIfAbsent( - repoId, - missingRepoId -> Caffeine.newBuilder() - .maximumSize(MAXIMUM_SIZE) - .build() - ); - } - - public Optional getLatestRunId(BenchmarkReadAccess benchmarkAccess, RepoId repoId, - CommitHash commitHash) { - - return getCacheForRepo(repoId).get( - commitHash, - missingHash -> benchmarkAccess.getLatestRunId(repoId, missingHash) - ); - } - - public Map getLatestRunIds(BenchmarkReadAccess benchmarkAccess, - RepoId repoId, Collection commitHashes) { - - return getCacheForRepo(repoId).getAll( - commitHashes, - missingHashesIterator -> { - List missingHashes = new ArrayList<>(); - missingHashesIterator.forEach(missingHashes::add); - Map missingIds = benchmarkAccess.getLatestRunIds(repoId, missingHashes); - return missingHashes.stream() - .collect(toMap( - it -> it, - it -> Optional.ofNullable(missingIds.get(it)) - )); - } - ).entrySet().stream() - .filter(entry -> entry.getValue().isPresent()) - .collect(toMap( - Entry::getKey, - entry -> entry.getValue().get() - )); - } - - public Optional getLatestRun(BenchmarkReadAccess benchmarkAccess, RunCache runCache, - RepoId repoId, CommitHash commitHash) { - - return getLatestRunId(benchmarkAccess, repoId, commitHash) - .map(runId -> runCache.getRun(benchmarkAccess, runId)); - } - - public Map getLatestRuns(BenchmarkReadAccess benchmarkAccess, RunCache runCache, - RepoId repoId, Collection commitHashes) { - - Map latestRunIds = getLatestRunIds(benchmarkAccess, repoId, commitHashes); - Map runs = runCache.getRuns(benchmarkAccess, latestRunIds.values()); - - return latestRunIds.entrySet().stream() - .collect(toMap( - Entry::getKey, - entry -> runs.get(entry.getValue()) - )); - } - - /** - * Invalidate the latest run for a specific commit. - * - * @param repoId the commit's repo - * @param commitHash the commit hash - */ - public void invalidate(RepoId repoId, CommitHash commitHash) { - getCacheForRepo(repoId).invalidate(commitHash); - } - - /** - * Invalidate an entire repo's latest runs. - * - * @param repoId the repo's id - */ - public void invalidate(RepoId repoId) { - cache.remove(repoId); - } + private static final int MAXIMUM_SIZE = 10000; + + private final ConcurrentHashMap>> cache; + + public LatestRunCache() { + cache = new ConcurrentHashMap<>(); + } + + private Cache> getCacheForRepo(RepoId repoId) { + return cache.computeIfAbsent( + repoId, missingRepoId -> Caffeine.newBuilder().maximumSize(MAXIMUM_SIZE).build()); + } + + public Optional getLatestRunId( + BenchmarkReadAccess benchmarkAccess, RepoId repoId, CommitHash commitHash) { + + return getCacheForRepo(repoId) + .get(commitHash, missingHash -> benchmarkAccess.getLatestRunId(repoId, missingHash)); + } + + public Map getLatestRunIds( + BenchmarkReadAccess benchmarkAccess, RepoId repoId, Collection commitHashes) { + + return getCacheForRepo(repoId) + .getAll( + commitHashes, + missingHashesIterator -> { + List missingHashes = new ArrayList<>(); + missingHashesIterator.forEach(missingHashes::add); + Map missingIds = + benchmarkAccess.getLatestRunIds(repoId, missingHashes); + return missingHashes.stream() + .collect(toMap(it -> it, it -> Optional.ofNullable(missingIds.get(it)))); + }) + .entrySet() + .stream() + .filter(entry -> entry.getValue().isPresent()) + .collect(toMap(Entry::getKey, entry -> entry.getValue().get())); + } + + public Optional getLatestRun( + BenchmarkReadAccess benchmarkAccess, + RunCache runCache, + RepoId repoId, + CommitHash commitHash) { + + return getLatestRunId(benchmarkAccess, repoId, commitHash) + .map(runId -> runCache.getRun(benchmarkAccess, runId)); + } + + public Map getLatestRuns( + BenchmarkReadAccess benchmarkAccess, + RunCache runCache, + RepoId repoId, + Collection commitHashes) { + + Map latestRunIds = getLatestRunIds(benchmarkAccess, repoId, commitHashes); + Map runs = runCache.getRuns(benchmarkAccess, latestRunIds.values()); + + return latestRunIds.entrySet().stream() + .collect(toMap(Entry::getKey, entry -> runs.get(entry.getValue()))); + } + + /** + * Invalidate the latest run for a specific commit. + * + * @param repoId the commit's repo + * @param commitHash the commit hash + */ + public void invalidate(RepoId repoId, CommitHash commitHash) { + getCacheForRepo(repoId).invalidate(commitHash); + } + + /** + * Invalidate an entire repo's latest runs. + * + * @param repoId the repo's id + */ + public void invalidate(RepoId repoId) { + cache.remove(repoId); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/RunCache.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/RunCache.java index fdbece5b0..1f3ec2cf2 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/RunCache.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/caches/RunCache.java @@ -21,46 +21,37 @@ */ public class RunCache { - private static final int MAXIMUM_SIZE = 10000; - - private final Cache cache; - - public RunCache() { - cache = Caffeine.newBuilder() - .maximumSize(MAXIMUM_SIZE) - .build(); - } - - public Run getRun(BenchmarkReadAccess benchmarkAccess, RunId runId) throws NoSuchRunException { - return cache.get(runId, benchmarkAccess::getRun); - } - - public Map getRuns(BenchmarkReadAccess benchmarkAccess, Collection runIds) { - return cache.getAll( - runIds, - missingIdIter -> { - List missingIds = new ArrayList<>(); - missingIdIter.forEach(missingIds::add); - return benchmarkAccess.getRuns(missingIds).stream() - .collect(toMap(Run::getId, it -> it)); - } - ); - } - - public List getRunsInOrder(BenchmarkReadAccess benchmarkReadAccess, - Collection runIds) { - - Map runs = getRuns(benchmarkReadAccess, runIds); - return runIds.stream() - .map(runs::get) - .filter(Objects::nonNull) - .collect(toList()); - } - - /** - * Invalidate all entries. - */ - public void invalidateAll() { - cache.invalidateAll(); - } + private static final int MAXIMUM_SIZE = 10000; + + private final Cache cache; + + public RunCache() { + cache = Caffeine.newBuilder().maximumSize(MAXIMUM_SIZE).build(); + } + + public Run getRun(BenchmarkReadAccess benchmarkAccess, RunId runId) throws NoSuchRunException { + return cache.get(runId, benchmarkAccess::getRun); + } + + public Map getRuns(BenchmarkReadAccess benchmarkAccess, Collection runIds) { + return cache.getAll( + runIds, + missingIdIter -> { + List missingIds = new ArrayList<>(); + missingIdIter.forEach(missingIds::add); + return benchmarkAccess.getRuns(missingIds).stream().collect(toMap(Run::getId, it -> it)); + }); + } + + public List getRunsInOrder( + BenchmarkReadAccess benchmarkReadAccess, Collection runIds) { + + Map runs = getRuns(benchmarkReadAccess, runIds); + return runIds.stream().map(runs::get).filter(Objects::nonNull).collect(toList()); + } + + /** Invalidate all entries. */ + public void invalidateAll() { + cache.invalidateAll(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccess.java index 8351daae7..e1e49f896 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccess.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -37,520 +36,532 @@ import org.jooq.codegen.db.tables.records.CommitRelationshipRecord; import org.jooq.codegen.db.tables.records.KnownCommitRecord; import org.jooq.exception.DataAccessException; -import org.jooq.exception.NoDataFoundException; -import org.jooq.exception.TooManyRowsException; -/** - * Provides read access to the git commits stored in the database. - */ +/** Provides read access to the git commits stored in the database. */ public class CommitReadAccess { - protected final DatabaseStorage databaseStorage; - - public CommitReadAccess(DatabaseStorage databaseStorage) { - this.databaseStorage = databaseStorage; - } - - private static Commit knownCommitRecordToCommit(KnownCommitRecord record) { - return new Commit( - RepoId.fromString(record.getRepoId()), - new CommitHash(record.getHash()), - record.getReachable(), - record.getTracked(), - record.getAuthor(), - record.getAuthorDate(), - record.getCommitter(), - record.getCommitterDate(), - record.getMessage() - ); - } - - private static FullCommit knownCommitRecordToFullCommit(KnownCommitRecord record, - Set parentHashes, Set childHashes) { - - return new FullCommit( - RepoId.fromString(record.getRepoId()), - new CommitHash(record.getHash()), - record.getReachable(), - record.getTracked(), - record.getAuthor(), - record.getAuthorDate(), - record.getCommitter(), - record.getCommitterDate(), - record.getMessage(), - parentHashes, - childHashes - ); - } - - private static FullCommit commitToFullCommit(Commit commit, Set parentHashes, - Set childHashes) { - - return new FullCommit( - commit.getRepoId(), - commit.getHash(), - commit.isReachable(), - commit.isTracked(), - commit.getAuthor(), - commit.getAuthorDate(), - commit.getCommitter(), - commit.getCommitterDate(), - commit.getMessage(), - parentHashes, - childHashes - ); - } - - public Commit getCommit(RepoId repoId, CommitHash commitHash) throws NoSuchCommitException { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - KnownCommitRecord record = db.dsl() - .fetchSingle( - KNOWN_COMMIT, - KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString()) - .and(KNOWN_COMMIT.HASH.eq(commitHash.getHash())) - ); - return knownCommitRecordToCommit(record); - } catch (DataAccessException e) { - throw new NoSuchCommitException(e, repoId, commitHash); - } - } - - /** - * Check if a commit exists. - * - * @param repoId the commit's repo id - * @param commitHash the commit's hash - * @throws NoSuchDimensionException if the commit doesn't exist - */ - public void guardCommitExists(RepoId repoId, CommitHash commitHash) - throws NoSuchDimensionException { - - getCommit(repoId, commitHash); - } - - /** - * Get multiple commits from a single repo at the same time. - * - * @param repoId the repo the commits ar in - * @param commitHashes the commits' hashes - * @return all commits which could be found. Commits could not be found are simply not included in - * this list. - */ - public List getCommits(RepoId repoId, Collection commitHashes) { - Set hashes = commitHashes.stream() - .map(CommitHash::getHash) - .collect(toSet()); - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(KNOWN_COMMIT) - .where(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())) - .and(KNOWN_COMMIT.HASH.in(hashes)) - .stream() - .map(CommitReadAccess::knownCommitRecordToCommit) - .collect(toList()); - } - } - - public FullCommit getFullCommit(RepoId repoId, CommitHash commitHash) - throws NoSuchCommitException { - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - KnownCommitRecord record = db.dsl() - .fetchSingle( - KNOWN_COMMIT, - KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString()) - .and(KNOWN_COMMIT.HASH.eq(commitHash.getHash())) - ); - - Set parentHashes = db.dsl() - .select(COMMIT_RELATIONSHIP.PARENT_HASH) - .from(COMMIT_RELATIONSHIP) - .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) - .and(COMMIT_RELATIONSHIP.CHILD_HASH.eq(commitHash.getHash())) - .stream() - .map(Record1::value1) - .map(CommitHash::new) - .collect(toSet()); - - Set childHashes = db.dsl() - .select(COMMIT_RELATIONSHIP.CHILD_HASH) - .from(COMMIT_RELATIONSHIP) - .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) - .and(COMMIT_RELATIONSHIP.PARENT_HASH.eq(commitHash.getHash())) - .stream() - .map(Record1::value1) - .map(CommitHash::new) - .collect(toSet()); - - return knownCommitRecordToFullCommit(record, parentHashes, childHashes); - } catch (DataAccessException e) { - throw new NoSuchCommitException(e, repoId, commitHash); - } - } - - public Set getParentHashes(RepoId repoId, CommitHash commitHash) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(COMMIT_RELATIONSHIP.PARENT_HASH) - .from(COMMIT_RELATIONSHIP) - .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) - .and(COMMIT_RELATIONSHIP.CHILD_HASH.eq(commitHash.getHash())) - .stream() - .map(Record1::value1) - .map(CommitHash::new) - .collect(toSet()); - } - } - - public Set getChildHashes(RepoId repoId, CommitHash commitHash) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(COMMIT_RELATIONSHIP.CHILD_HASH) - .from(COMMIT_RELATIONSHIP) - .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) - .and(COMMIT_RELATIONSHIP.PARENT_HASH.eq(commitHash.getHash())) - .stream() - .map(Record1::value1) - .map(CommitHash::new) - .collect(toSet()); - } - } - - /** - * Convert a list of {@link Commit}s to {@link FullCommit}s. If a commit's parents or children - * could not be found, they default to the empty set. - * - * @param commits the commits to promote - * @return the promoted commits - */ - public List promoteCommits(List commits) { - Map> commitsByRepo = commits.stream() - .collect(groupingBy(Commit::getRepoId, toSet())); - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - List result = new ArrayList<>(); - - for (Entry> entry : commitsByRepo.entrySet()) { - Set hashes = entry.getValue().stream() - .map(Commit::getHash) - .map(CommitHash::getHash) - .collect(toSet()); - - Map> allParentHashes = db.dsl() - .selectFrom(COMMIT_RELATIONSHIP) - .where(COMMIT_RELATIONSHIP.REPO_ID.eq(entry.getKey().getIdAsString())) - .and(COMMIT_RELATIONSHIP.CHILD_HASH.in(hashes)) - .stream() - .collect(groupingBy( - CommitRelationshipRecord::getChildHash, - mapping(it -> new CommitHash(it.getParentHash()), toSet()) - )); - - Map> allChildHashes = db.dsl() - .selectFrom(COMMIT_RELATIONSHIP) - .where(COMMIT_RELATIONSHIP.REPO_ID.eq(entry.getKey().getIdAsString())) - .and(COMMIT_RELATIONSHIP.PARENT_HASH.in(hashes)) - .stream() - .collect(groupingBy( - CommitRelationshipRecord::getParentHash, - mapping(it -> new CommitHash(it.getChildHash()), toSet()) - )); - - entry.getValue().stream() - .map(commit -> commitToFullCommit( - commit, - allParentHashes.getOrDefault(commit.getHashAsString(), Set.of()), - allChildHashes.getOrDefault(commit.getHashAsString(), Set.of()) - )) - .forEach(result::add); - } - - return result; - } - } - - /** - * Gets all tracked commits whose committer date is between the given start and end - * time. - * - * @param repoId the id of the repo - * @param startTime the start committer time - * @param stopTime the stop committer time - * @return all tracked commits whose committer date is between the given start and end time in no - * particular order - */ - public List getTrackedCommitsBetween(RepoId repoId, @Nullable Instant startTime, - @Nullable Instant stopTime) { - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - SelectConditionStep query = db.dsl() - .selectFrom(KNOWN_COMMIT) - .where(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())) - .and(KNOWN_COMMIT.TRACKED); - - if (startTime != null) { - query = query.and(KNOWN_COMMIT.COMMITTER_DATE.ge(startTime)); - } - if (stopTime != null) { - query = query.and(KNOWN_COMMIT.COMMITTER_DATE.le(stopTime)); - } - - return query.stream() - .map(CommitReadAccess::knownCommitRecordToCommit) - .collect(toList()); - } - } - - /** - * @param repoId the id of the root commit's repo - * @param rootHash the hash of the root commit - * @return all tracked commits descending from the given root. - */ - public List getDescendantCommits(RepoId repoId, CommitHash rootHash) { - String query = "WITH RECURSIVE\n" - + "\n" - + "initial(hash) AS (\n" - + " VALUES (?)\n" // <-- Binding #1 - Root hash - + "),\n" - + "\n" - + "rec(hash) AS (\n" - + " SELECT initial.hash\n" - + " FROM initial\n" - + " JOIN known_commit\n" - + " ON initial.hash = known_commit.hash\n" - + " WHERE known_commit.repo_id = ?\n" // <-- Binding #2 - repo id - + " \n" - + " UNION\n" - + " \n" - + " SELECT commit_relationship.child_hash\n" - + " FROM commit_relationship\n" - + " JOIN rec\n" - + " ON rec.hash = commit_relationship.parent_hash\n" - + " WHERE repo_id = ?\n" // <-- Binding #3 - repo id - + ")\n" - + "\n" - + "SELECT rec.hash \n" - + "FROM rec\n" - + "JOIN known_commit\n" - + " ON rec.hash = known_commit.hash\n" - + "WHERE known_commit.tracked\n" - + ""; - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .fetchLazy(query, rootHash.getHash(), repoId.getIdAsString(), repoId.getIdAsString()) - .stream() - .map(record -> (String) record.getValue(0)) - .map(CommitHash::new) - .collect(toList()); - } - } - - public List getFirstParentsOfBranch(RepoId repoId, BranchName branch, - CommitHash rootHash) { - - String query = "WITH RECURSIVE\n" - // Commits reachable from the branch - + "reachable(hash) AS (\n" - + " SELECT branch.latest_commit_hash\n" - + " FROM branch\n" - + " WHERE branch.repo_id = ?\n" // <-- Binding #1 - repo id - + " AND branch.name = ?\n" // <-- Binding #2 - branch name - + " \n" - + " UNION\n" - + " \n" - + " SELECT commit_relationship.parent_hash\n" - + " FROM commit_relationship\n" - + " JOIN reachable\n" - + " ON reachable.hash = commit_relationship.child_hash\n" - + " WHERE commit_relationship.repo_id = ?\n" // <-- Binding #3 - repo id - + "),\n" - + "\n" - // The starting commit - + "initial(hash) AS (\n" - + " VALUES (?)\n" // <-- Binding #4 - hash of starting commit - + "),\n" - + "\n" - // Parents of the starting commit that are not reachable from the branch - + "parents(hash) AS (\n" - + " SELECT initial.hash\n" - + " FROM initial\n" - + " JOIN known_commit\n" - + " ON initial.hash = known_commit.hash\n" - + " WHERE known_commit.repo_id = ?\n" // <-- Binding #5 - repo id - + " AND initial.hash NOT IN reachable\n" - + " \n" - + " UNION\n" - + " \n" - + " SELECT commit_relationship.parent_hash\n" - + " FROM commit_relationship\n" - + " JOIN parents\n" - + " ON parents.hash = commit_relationship.child_hash\n" - + " WHERE commit_relationship.repo_id = ?\n" // <-- Binding #6 - repo id - + " AND commit_relationship.parent_hash NOT IN reachable\n" - + ")\n" - + "\n" - + "SELECT commit_relationship.parent_hash\n" - + "FROM commit_relationship\n" - + " JOIN parents\n" - + " ON parents.hash = commit_relationship.child_hash\n" - + " JOIN reachable\n" - + " ON reachable.hash = commit_relationship.parent_hash\n" - + ""; - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .fetchStream( - query, - repoId.getIdAsString(), - branch.getName(), - repoId.getIdAsString(), - rootHash.getHash(), - repoId.getIdAsString(), - repoId.getIdAsString() - ).map(record -> record.get(0, String.class)) - .map(CommitHash::new) - .collect(toList()); - } - } - - /** - * Gets all commits - tracked and untracked - whose committer date is between the given - * start and end time. - * - * @param repoId the id of the repo - * @param branchNames the names of all branches to search for commits - * @param startTime the start author time - * @param stopTime the stop author time - * @return all commits whose author date is between the given start and end time in no particular - * order - */ - public List getCommitsBetween(RepoId repoId, Collection branchNames, - @Nullable Instant startTime, @Nullable Instant stopTime) { - - // Kinda ugly but it works and is also relatively fast, so I'm fine with it. - - // Part 1: Recursive query - // - // This generates a recursive query with variable number of bindings as a string and then - // executes it. This is, of course, pretty ugly but I can't get jOOQ to generate correct - // recursive queries in the sqlite dialect. - - List branchNamesStr = branchNames.stream() - .map(BranchName::getName) - .collect(toList()); - - String branchNamesBindings = branchNames.stream() - .map(s -> "?") - .collect(Collectors.joining(", ")); - - String queryStr = "" - + "WITH RECURSIVE rec(hash) AS (\n" - + " SELECT branch.latest_commit_hash" - + " FROM branch" - + " WHERE branch.repo_id = ?" // <-- Binding #1 - + " AND branch.name IN (" + branchNamesBindings + ")\n" // <-- Binding #2 (multiple values) - + " \n" - + " UNION\n" - + " \n" - + " SELECT commit_relationship.parent_hash\n" - + " FROM commit_relationship\n" - + " JOIN rec\n" - + " ON rec.hash = commit_relationship.child_hash\n" - + " WHERE commit_relationship.repo_id = ?\n" // <-- Binding #3 - + ")\n" - + "\n" - + "SELECT rec.hash\n" - + "FROM rec\n" - + ""; - List bindings = new ArrayList<>(); - bindings.add(repoId.getIdAsString()); - bindings.addAll(branchNamesStr); - bindings.add(repoId.getIdAsString()); - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - Set reachableHashes = db.dsl() - .fetch(queryStr, bindings.toArray()) - .stream() - .map(record -> record.get(0, String.class)) - .collect(toSet()); - - // Part 2: Fetching commits - // - // This part fetches the reachable commits we're after based on the hashes returned from the - // previous query with a normal jOOQ query. This filtering isn't included in the above - // recursive query because when we tried to do that, jdbc/jOOQ failed to correctly parse the - // timestamps for the author/committer dates for some reason. - - SelectConditionStep query = db.dsl() - .selectFrom(KNOWN_COMMIT) - .where(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())) - .and(KNOWN_COMMIT.HASH.in(reachableHashes)); - - if (startTime != null) { - query = query.and(KNOWN_COMMIT.COMMITTER_DATE.ge(startTime)); - } - if (stopTime != null) { - query = query.and(KNOWN_COMMIT.COMMITTER_DATE.le(stopTime)); - } - - return query.stream() - .map(CommitReadAccess::knownCommitRecordToCommit) - .collect(toList()); - } - } - - // TODO: 06.06.21 Add tests for this function - public List> searchCommits(int limit, @Nullable RepoId repoId, - String queryStr) { - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - var query = db.dsl() - .select( - KNOWN_COMMIT.REPO_ID, - KNOWN_COMMIT.HASH, - KNOWN_COMMIT.REACHABLE, - KNOWN_COMMIT.TRACKED, - KNOWN_COMMIT.AUTHOR, - KNOWN_COMMIT.AUTHOR_DATE, - KNOWN_COMMIT.COMMITTER, - KNOWN_COMMIT.COMMITTER_DATE, - KNOWN_COMMIT.MESSAGE, - field(exists(select(one()) - .from(RUN) - .where(RUN.REPO_ID.eq(KNOWN_COMMIT.REPO_ID)) - .and(RUN.COMMIT_HASH.eq(KNOWN_COMMIT.HASH)) - )) - ) - .from(KNOWN_COMMIT) - .where(KNOWN_COMMIT.HASH.contains(queryStr) - .or(KNOWN_COMMIT.MESSAGE.contains(queryStr)) - .or(KNOWN_COMMIT.AUTHOR.contains(queryStr)) - .or(KNOWN_COMMIT.COMMITTER.contains(queryStr))); - - if (repoId != null) { - query = query.and(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())); - } - - return query.orderBy(KNOWN_COMMIT.COMMITTER_DATE.desc()) - .limit(limit) - .stream() - .map(record -> new Pair<>( - new Commit( - RepoId.fromString(record.value1()), - new CommitHash(record.value2()), - record.value3(), - record.value4(), - record.value5(), - record.value6(), - record.value7(), - record.value8(), - record.value9() - ), - record.value10() - )) - .collect(toList()); - } - } + protected final DatabaseStorage databaseStorage; + + public CommitReadAccess(DatabaseStorage databaseStorage) { + this.databaseStorage = databaseStorage; + } + + private static Commit knownCommitRecordToCommit(KnownCommitRecord record) { + return new Commit( + RepoId.fromString(record.getRepoId()), + new CommitHash(record.getHash()), + record.getReachable(), + record.getTracked(), + record.getAuthor(), + record.getAuthorDate(), + record.getCommitter(), + record.getCommitterDate(), + record.getMessage()); + } + + private static FullCommit knownCommitRecordToFullCommit( + KnownCommitRecord record, Set parentHashes, Set childHashes) { + + return new FullCommit( + RepoId.fromString(record.getRepoId()), + new CommitHash(record.getHash()), + record.getReachable(), + record.getTracked(), + record.getAuthor(), + record.getAuthorDate(), + record.getCommitter(), + record.getCommitterDate(), + record.getMessage(), + parentHashes, + childHashes); + } + + private static FullCommit commitToFullCommit( + Commit commit, Set parentHashes, Set childHashes) { + + return new FullCommit( + commit.getRepoId(), + commit.getHash(), + commit.isReachable(), + commit.isTracked(), + commit.getAuthor(), + commit.getAuthorDate(), + commit.getCommitter(), + commit.getCommitterDate(), + commit.getMessage(), + parentHashes, + childHashes); + } + + public Commit getCommit(RepoId repoId, CommitHash commitHash) throws NoSuchCommitException { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + KnownCommitRecord record = + db.dsl() + .fetchSingle( + KNOWN_COMMIT, + KNOWN_COMMIT + .REPO_ID + .eq(repoId.getIdAsString()) + .and(KNOWN_COMMIT.HASH.eq(commitHash.getHash()))); + return knownCommitRecordToCommit(record); + } catch (DataAccessException e) { + throw new NoSuchCommitException(e, repoId, commitHash); + } + } + + /** + * Check if a commit exists. + * + * @param repoId the commit's repo id + * @param commitHash the commit's hash + * @throws NoSuchDimensionException if the commit doesn't exist + */ + public void guardCommitExists(RepoId repoId, CommitHash commitHash) + throws NoSuchDimensionException { + + getCommit(repoId, commitHash); + } + + /** + * Get multiple commits from a single repo at the same time. + * + * @param repoId the repo the commits ar in + * @param commitHashes the commits' hashes + * @return all commits which could be found. Commits could not be found are simply not included in + * this list. + */ + public List getCommits(RepoId repoId, Collection commitHashes) { + Set hashes = commitHashes.stream().map(CommitHash::getHash).collect(toSet()); + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .selectFrom(KNOWN_COMMIT) + .where(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())) + .and(KNOWN_COMMIT.HASH.in(hashes)) + .stream() + .map(CommitReadAccess::knownCommitRecordToCommit) + .collect(toList()); + } + } + + public FullCommit getFullCommit(RepoId repoId, CommitHash commitHash) + throws NoSuchCommitException { + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + KnownCommitRecord record = + db.dsl() + .fetchSingle( + KNOWN_COMMIT, + KNOWN_COMMIT + .REPO_ID + .eq(repoId.getIdAsString()) + .and(KNOWN_COMMIT.HASH.eq(commitHash.getHash()))); + + Set parentHashes = + db + .dsl() + .select(COMMIT_RELATIONSHIP.PARENT_HASH) + .from(COMMIT_RELATIONSHIP) + .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) + .and(COMMIT_RELATIONSHIP.CHILD_HASH.eq(commitHash.getHash())) + .stream() + .map(Record1::value1) + .map(CommitHash::new) + .collect(toSet()); + + Set childHashes = + db + .dsl() + .select(COMMIT_RELATIONSHIP.CHILD_HASH) + .from(COMMIT_RELATIONSHIP) + .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) + .and(COMMIT_RELATIONSHIP.PARENT_HASH.eq(commitHash.getHash())) + .stream() + .map(Record1::value1) + .map(CommitHash::new) + .collect(toSet()); + + return knownCommitRecordToFullCommit(record, parentHashes, childHashes); + } catch (DataAccessException e) { + throw new NoSuchCommitException(e, repoId, commitHash); + } + } + + public Set getParentHashes(RepoId repoId, CommitHash commitHash) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .select(COMMIT_RELATIONSHIP.PARENT_HASH) + .from(COMMIT_RELATIONSHIP) + .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) + .and(COMMIT_RELATIONSHIP.CHILD_HASH.eq(commitHash.getHash())) + .stream() + .map(Record1::value1) + .map(CommitHash::new) + .collect(toSet()); + } + } + + public Set getChildHashes(RepoId repoId, CommitHash commitHash) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .select(COMMIT_RELATIONSHIP.CHILD_HASH) + .from(COMMIT_RELATIONSHIP) + .where(COMMIT_RELATIONSHIP.REPO_ID.eq(repoId.getIdAsString())) + .and(COMMIT_RELATIONSHIP.PARENT_HASH.eq(commitHash.getHash())) + .stream() + .map(Record1::value1) + .map(CommitHash::new) + .collect(toSet()); + } + } + + /** + * Convert a list of {@link Commit}s to {@link FullCommit}s. If a commit's parents or children + * could not be found, they default to the empty set. + * + * @param commits the commits to promote + * @return the promoted commits + */ + public List promoteCommits(List commits) { + Map> commitsByRepo = + commits.stream().collect(groupingBy(Commit::getRepoId, toSet())); + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + List result = new ArrayList<>(); + + for (Entry> entry : commitsByRepo.entrySet()) { + Set hashes = + entry.getValue().stream() + .map(Commit::getHash) + .map(CommitHash::getHash) + .collect(toSet()); + + Map> allParentHashes = + db + .dsl() + .selectFrom(COMMIT_RELATIONSHIP) + .where(COMMIT_RELATIONSHIP.REPO_ID.eq(entry.getKey().getIdAsString())) + .and(COMMIT_RELATIONSHIP.CHILD_HASH.in(hashes)) + .stream() + .collect( + groupingBy( + CommitRelationshipRecord::getChildHash, + mapping(it -> new CommitHash(it.getParentHash()), toSet()))); + + Map> allChildHashes = + db + .dsl() + .selectFrom(COMMIT_RELATIONSHIP) + .where(COMMIT_RELATIONSHIP.REPO_ID.eq(entry.getKey().getIdAsString())) + .and(COMMIT_RELATIONSHIP.PARENT_HASH.in(hashes)) + .stream() + .collect( + groupingBy( + CommitRelationshipRecord::getParentHash, + mapping(it -> new CommitHash(it.getChildHash()), toSet()))); + + entry.getValue().stream() + .map( + commit -> + commitToFullCommit( + commit, + allParentHashes.getOrDefault(commit.getHashAsString(), Set.of()), + allChildHashes.getOrDefault(commit.getHashAsString(), Set.of()))) + .forEach(result::add); + } + + return result; + } + } + + /** + * Gets all tracked commits whose committer date is between the given start and end time. + * + * @param repoId the id of the repo + * @param startTime the start committer time + * @param stopTime the stop committer time + * @return all tracked commits whose committer date is between the given start and end time in no + * particular order + */ + public List getTrackedCommitsBetween( + RepoId repoId, @Nullable Instant startTime, @Nullable Instant stopTime) { + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + SelectConditionStep query = + db.dsl() + .selectFrom(KNOWN_COMMIT) + .where(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())) + .and(KNOWN_COMMIT.TRACKED); + + if (startTime != null) { + query = query.and(KNOWN_COMMIT.COMMITTER_DATE.ge(startTime)); + } + if (stopTime != null) { + query = query.and(KNOWN_COMMIT.COMMITTER_DATE.le(stopTime)); + } + + return query.stream().map(CommitReadAccess::knownCommitRecordToCommit).collect(toList()); + } + } + + /** + * @param repoId the id of the root commit's repo + * @param rootHash the hash of the root commit + * @return all tracked commits descending from the given root. + */ + public List getDescendantCommits(RepoId repoId, CommitHash rootHash) { + String query = + "WITH RECURSIVE\n" + + "\n" + + "initial(hash) AS (\n" + + " VALUES (?)\n" // <-- Binding #1 - Root hash + + "),\n" + + "\n" + + "rec(hash) AS (\n" + + " SELECT initial.hash\n" + + " FROM initial\n" + + " JOIN known_commit\n" + + " ON initial.hash = known_commit.hash\n" + + " WHERE known_commit.repo_id = ?\n" // <-- Binding #2 - repo id + + " \n" + + " UNION\n" + + " \n" + + " SELECT commit_relationship.child_hash\n" + + " FROM commit_relationship\n" + + " JOIN rec\n" + + " ON rec.hash = commit_relationship.parent_hash\n" + + " WHERE repo_id = ?\n" // <-- Binding #3 - repo id + + ")\n" + + "\n" + + "SELECT rec.hash \n" + + "FROM rec\n" + + "JOIN known_commit\n" + + " ON rec.hash = known_commit.hash\n" + + "WHERE known_commit.tracked\n" + + ""; + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .fetchLazy(query, rootHash.getHash(), repoId.getIdAsString(), repoId.getIdAsString()) + .stream() + .map(record -> (String) record.getValue(0)) + .map(CommitHash::new) + .collect(toList()); + } + } + + public List getFirstParentsOfBranch( + RepoId repoId, BranchName branch, CommitHash rootHash) { + + String query = + "WITH RECURSIVE\n" + // Commits reachable from the branch + + "reachable(hash) AS (\n" + + " SELECT branch.latest_commit_hash\n" + + " FROM branch\n" + + " WHERE branch.repo_id = ?\n" // <-- Binding #1 - repo id + + " AND branch.name = ?\n" // <-- Binding #2 - branch name + + " \n" + + " UNION\n" + + " \n" + + " SELECT commit_relationship.parent_hash\n" + + " FROM commit_relationship\n" + + " JOIN reachable\n" + + " ON reachable.hash = commit_relationship.child_hash\n" + + " WHERE commit_relationship.repo_id = ?\n" // <-- Binding #3 - repo id + + "),\n" + + "\n" + // The starting commit + + "initial(hash) AS (\n" + + " VALUES (?)\n" // <-- Binding #4 - hash of starting commit + + "),\n" + + "\n" + // Parents of the starting commit that are not reachable from the branch + + "parents(hash) AS (\n" + + " SELECT initial.hash\n" + + " FROM initial\n" + + " JOIN known_commit\n" + + " ON initial.hash = known_commit.hash\n" + + " WHERE known_commit.repo_id = ?\n" // <-- Binding #5 - repo id + + " AND initial.hash NOT IN reachable\n" + + " \n" + + " UNION\n" + + " \n" + + " SELECT commit_relationship.parent_hash\n" + + " FROM commit_relationship\n" + + " JOIN parents\n" + + " ON parents.hash = commit_relationship.child_hash\n" + + " WHERE commit_relationship.repo_id = ?\n" // <-- Binding #6 - repo id + + " AND commit_relationship.parent_hash NOT IN reachable\n" + + ")\n" + + "\n" + + "SELECT commit_relationship.parent_hash\n" + + "FROM commit_relationship\n" + + " JOIN parents\n" + + " ON parents.hash = commit_relationship.child_hash\n" + + " JOIN reachable\n" + + " ON reachable.hash = commit_relationship.parent_hash\n" + + ""; + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .fetchStream( + query, + repoId.getIdAsString(), + branch.getName(), + repoId.getIdAsString(), + rootHash.getHash(), + repoId.getIdAsString(), + repoId.getIdAsString()) + .map(record -> record.get(0, String.class)) + .map(CommitHash::new) + .collect(toList()); + } + } + + /** + * Gets all commits - tracked and untracked - whose committer date is between the given + * start and end time. + * + * @param repoId the id of the repo + * @param branchNames the names of all branches to search for commits + * @param startTime the start author time + * @param stopTime the stop author time + * @return all commits whose author date is between the given start and end time in no particular + * order + */ + public List getCommitsBetween( + RepoId repoId, + Collection branchNames, + @Nullable Instant startTime, + @Nullable Instant stopTime) { + + // Kinda ugly but it works and is also relatively fast, so I'm fine with it. + + // Part 1: Recursive query + // + // This generates a recursive query with variable number of bindings as a string and then + // executes it. This is, of course, pretty ugly but I can't get jOOQ to generate correct + // recursive queries in the sqlite dialect. + + List branchNamesStr = branchNames.stream().map(BranchName::getName).collect(toList()); + + String branchNamesBindings = + branchNames.stream().map(s -> "?").collect(Collectors.joining(", ")); + + String queryStr = + "" + + "WITH RECURSIVE rec(hash) AS (\n" + + " SELECT branch.latest_commit_hash" + + " FROM branch" + + " WHERE branch.repo_id = ?" // <-- Binding #1 + + " AND branch.name IN (" + + branchNamesBindings + + ")\n" // <-- Binding #2 (multiple values) + + " \n" + + " UNION\n" + + " \n" + + " SELECT commit_relationship.parent_hash\n" + + " FROM commit_relationship\n" + + " JOIN rec\n" + + " ON rec.hash = commit_relationship.child_hash\n" + + " WHERE commit_relationship.repo_id = ?\n" // <-- Binding #3 + + ")\n" + + "\n" + + "SELECT rec.hash\n" + + "FROM rec\n" + + ""; + List bindings = new ArrayList<>(); + bindings.add(repoId.getIdAsString()); + bindings.addAll(branchNamesStr); + bindings.add(repoId.getIdAsString()); + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + Set reachableHashes = + db.dsl().fetch(queryStr, bindings.toArray()).stream() + .map(record -> record.get(0, String.class)) + .collect(toSet()); + + // Part 2: Fetching commits + // + // This part fetches the reachable commits we're after based on the hashes returned from the + // previous query with a normal jOOQ query. This filtering isn't included in the above + // recursive query because when we tried to do that, jdbc/jOOQ failed to correctly parse the + // timestamps for the author/committer dates for some reason. + + SelectConditionStep query = + db.dsl() + .selectFrom(KNOWN_COMMIT) + .where(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())) + .and(KNOWN_COMMIT.HASH.in(reachableHashes)); + + if (startTime != null) { + query = query.and(KNOWN_COMMIT.COMMITTER_DATE.ge(startTime)); + } + if (stopTime != null) { + query = query.and(KNOWN_COMMIT.COMMITTER_DATE.le(stopTime)); + } + + return query.stream().map(CommitReadAccess::knownCommitRecordToCommit).collect(toList()); + } + } + + // TODO: 06.06.21 Add tests for this function + public List> searchCommits( + int limit, @Nullable RepoId repoId, String queryStr) { + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + var query = + db.dsl() + .select( + KNOWN_COMMIT.REPO_ID, + KNOWN_COMMIT.HASH, + KNOWN_COMMIT.REACHABLE, + KNOWN_COMMIT.TRACKED, + KNOWN_COMMIT.AUTHOR, + KNOWN_COMMIT.AUTHOR_DATE, + KNOWN_COMMIT.COMMITTER, + KNOWN_COMMIT.COMMITTER_DATE, + KNOWN_COMMIT.MESSAGE, + field( + exists( + select(one()) + .from(RUN) + .where(RUN.REPO_ID.eq(KNOWN_COMMIT.REPO_ID)) + .and(RUN.COMMIT_HASH.eq(KNOWN_COMMIT.HASH))))) + .from(KNOWN_COMMIT) + .where( + KNOWN_COMMIT + .HASH + .contains(queryStr) + .or(KNOWN_COMMIT.MESSAGE.contains(queryStr)) + .or(KNOWN_COMMIT.AUTHOR.contains(queryStr)) + .or(KNOWN_COMMIT.COMMITTER.contains(queryStr))); + + if (repoId != null) { + query = query.and(KNOWN_COMMIT.REPO_ID.eq(repoId.getIdAsString())); + } + + return query.orderBy(KNOWN_COMMIT.COMMITTER_DATE.desc()).limit(limit).stream() + .map( + record -> + new Pair<>( + new Commit( + RepoId.fromString(record.value1()), + new CommitHash(record.value2()), + record.value3(), + record.value4(), + record.value5(), + record.value6(), + record.value7(), + record.value8(), + record.value9()), + record.value10())) + .collect(toList()); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/Commit.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/Commit.java index e660ff181..b1b1cb7b1 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/Commit.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/Commit.java @@ -5,124 +5,121 @@ import java.time.Instant; import java.util.Optional; -/** - * A git commit. For a version that includes parent and child hashes, see {@link FullCommit}. - */ +/** A git commit. For a version that includes parent and child hashes, see {@link FullCommit}. */ public class Commit { - private final RepoId repoId; - private final CommitHash hash; - private final boolean reachable; - private final boolean tracked; - private final String author; - private final Instant authorDate; - private final String committer; - private final Instant committerDate; - private final String message; - - public Commit(RepoId repoId, CommitHash hash, boolean reachable, boolean tracked, String author, - Instant authorDate, String committer, Instant committerDate, String message) { - - this.repoId = repoId; - this.hash = hash; - this.reachable = reachable; - this.tracked = tracked; - this.author = author; - this.authorDate = authorDate; - this.committer = committer; - this.committerDate = committerDate; - this.message = message; - } - - /** - * Returns a placeholder commit to be used in situations where you absolutely need a {@link - * Commit} but don't have any available. - * - * @param repoId the commit's repo id - * @param hash the commit's hash - * @return a placeholder commit with the specified repo id and hash and placeholder values in all - * other fields. - */ - public static Commit placeholder(RepoId repoId, CommitHash hash) { - return new Commit( - repoId, - hash, - false, - false, - "N/A", - Instant.EPOCH, - "N/A", - Instant.EPOCH, - "N/A" - ); - } - - public RepoId getRepoId() { - return repoId; - } - - public CommitHash getHash() { - return hash; - } - - public String getHashAsString() { - return hash.getHash(); - } - - /** - * @return whether this commit can be reached from any branch. - */ - public boolean isReachable() { - return reachable; - } - - /** - * A commit is tracked if it is reachable from any tracked branch. - * - * @return whether this commit is tracked - */ - public boolean isTracked() { - return tracked; - } - - public String getAuthor() { - return author; - } - - public Instant getAuthorDate() { - return authorDate; - } - - public String getCommitter() { - return committer; - } - - public Instant getCommitterDate() { - return committerDate; - } - - public String getMessage() { - return message; - } - - public static Pair> splitMessageIntoSections(String message) { - String[] split = message.split("\n\n", 2); - if (split.length == 2) { - return new Pair<>(split[0] + "\n", Optional.of(split[1])); - } else { - return new Pair<>(message, Optional.empty()); - } - } - - public Pair> getSections() { - return splitMessageIntoSections(message); - } - - public String getSummary() { - return getSections().getFirst(); - } - - public Optional getMessageWithoutSummary() { - return getSections().getSecond(); - } + private final RepoId repoId; + private final CommitHash hash; + private final boolean reachable; + private final boolean tracked; + private final String author; + private final Instant authorDate; + private final String committer; + private final Instant committerDate; + private final String message; + + public Commit( + RepoId repoId, + CommitHash hash, + boolean reachable, + boolean tracked, + String author, + Instant authorDate, + String committer, + Instant committerDate, + String message) { + + this.repoId = repoId; + this.hash = hash; + this.reachable = reachable; + this.tracked = tracked; + this.author = author; + this.authorDate = authorDate; + this.committer = committer; + this.committerDate = committerDate; + this.message = message; + } + + /** + * Returns a placeholder commit to be used in situations where you absolutely need a {@link + * Commit} but don't have any available. + * + * @param repoId the commit's repo id + * @param hash the commit's hash + * @return a placeholder commit with the specified repo id and hash and placeholder values in all + * other fields. + */ + public static Commit placeholder(RepoId repoId, CommitHash hash) { + return new Commit( + repoId, hash, false, false, "N/A", Instant.EPOCH, "N/A", Instant.EPOCH, "N/A"); + } + + public RepoId getRepoId() { + return repoId; + } + + public CommitHash getHash() { + return hash; + } + + public String getHashAsString() { + return hash.getHash(); + } + + /** + * @return whether this commit can be reached from any branch. + */ + public boolean isReachable() { + return reachable; + } + + /** + * A commit is tracked if it is reachable from any tracked branch. + * + * @return whether this commit is tracked + */ + public boolean isTracked() { + return tracked; + } + + public String getAuthor() { + return author; + } + + public Instant getAuthorDate() { + return authorDate; + } + + public String getCommitter() { + return committer; + } + + public Instant getCommitterDate() { + return committerDate; + } + + public String getMessage() { + return message; + } + + public static Pair> splitMessageIntoSections(String message) { + String[] split = message.split("\n\n", 2); + if (split.length == 2) { + return new Pair<>(split[0] + "\n", Optional.of(split[1])); + } else { + return new Pair<>(message, Optional.empty()); + } + } + + public Pair> getSections() { + return splitMessageIntoSections(message); + } + + public String getSummary() { + return getSections().getFirst(); + } + + public Optional getMessageWithoutSummary() { + return getSections().getSecond(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitHash.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitHash.java index cddd1d9a4..758ebce68 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitHash.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitHash.java @@ -2,43 +2,38 @@ import java.util.Objects; -/** - * A git commit hash (40 characters of hexadecimal digits). - */ +/** A git commit hash (40 characters of hexadecimal digits). */ public class CommitHash { - private final String hash; - - public CommitHash(String hash) { - this.hash = hash; - } - - public String getHash() { - return hash; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CommitHash that = (CommitHash) o; - return Objects.equals(hash, that.hash); - } - - @Override - public int hashCode() { - return Objects.hash(hash); - } - - @Override - public String toString() { - return "CommitHash{" + - "hash='" + hash + '\'' + - '}'; - } - + private final String hash; + + public CommitHash(String hash) { + this.hash = hash; + } + + public String getHash() { + return hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CommitHash that = (CommitHash) o; + return Objects.equals(hash, that.hash); + } + + @Override + public int hashCode() { + return Objects.hash(hash); + } + + @Override + public String toString() { + return "CommitHash{" + "hash='" + hash + '\'' + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/FullCommit.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/FullCommit.java index 2ed0a2bf1..55e51a148 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/FullCommit.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/FullCommit.java @@ -4,29 +4,36 @@ import java.time.Instant; import java.util.Set; -/** - * A git commit with parent and child hashes. - */ +/** A git commit with parent and child hashes. */ public class FullCommit extends Commit { - private final Set parentHashes; - private final Set childHashes; - - public FullCommit(RepoId repoId, CommitHash hash, boolean reachable, boolean tracked, - String author, Instant authorDate, String committer, Instant committerDate, String message, - Set parentHashes, Set childHashes) { - - super(repoId, hash, reachable, tracked, author, authorDate, committer, committerDate, message); - - this.parentHashes = parentHashes; - this.childHashes = childHashes; - } - - public Set getParentHashes() { - return parentHashes; - } - - public Set getChildHashes() { - return childHashes; - } + private final Set parentHashes; + private final Set childHashes; + + public FullCommit( + RepoId repoId, + CommitHash hash, + boolean reachable, + boolean tracked, + String author, + Instant authorDate, + String committer, + Instant committerDate, + String message, + Set parentHashes, + Set childHashes) { + + super(repoId, hash, reachable, tracked, author, authorDate, committer, committerDate, message); + + this.parentHashes = parentHashes; + this.childHashes = childHashes; + } + + public Set getParentHashes() { + return parentHashes; + } + + public Set getChildHashes() { + return childHashes; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/exceptions/NoSuchCommitException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/exceptions/NoSuchCommitException.java index 2545064b6..f062a2fc7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/exceptions/NoSuchCommitException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/committaccess/exceptions/NoSuchCommitException.java @@ -9,21 +9,21 @@ */ public class NoSuchCommitException extends RuntimeException { - private final RepoId repoId; - private final CommitHash commitHash; + private final RepoId repoId; + private final CommitHash commitHash; - public NoSuchCommitException(Throwable t, RepoId repoId, CommitHash commitHash) { - super("no commit in repo " + repoId + " with hash " + commitHash, t); + public NoSuchCommitException(Throwable t, RepoId repoId, CommitHash commitHash) { + super("no commit in repo " + repoId + " with hash " + commitHash, t); - this.repoId = repoId; - this.commitHash = commitHash; - } + this.repoId = repoId; + this.commitHash = commitHash; + } - public RepoId getRepoId() { - return repoId; - } + public RepoId getRepoId() { + return repoId; + } - public CommitHash getCommitHash() { - return commitHash; - } + public CommitHash getCommitHash() { + return commitHash; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccess.java index c0125803d..f439a6c50 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccess.java @@ -29,258 +29,241 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Provides read access to the global list of dimensions and their associated information. - */ +/** Provides read access to the global list of dimensions and their associated information. */ public class DimensionReadAccess { - private final Logger LOGGER = LoggerFactory.getLogger(DimensionReadAccess.class); + private final Logger LOGGER = LoggerFactory.getLogger(DimensionReadAccess.class); - protected final DatabaseStorage databaseStorage; + protected final DatabaseStorage databaseStorage; - public DimensionReadAccess(DatabaseStorage databaseStorage) { - this.databaseStorage = databaseStorage; - } + public DimensionReadAccess(DatabaseStorage databaseStorage) { + this.databaseStorage = databaseStorage; + } - private static Dimension dimRecordToDim(DimensionRecord record) { - return new Dimension(record.getBenchmark(), record.getMetric()); - } + private static Dimension dimRecordToDim(DimensionRecord record) { + return new Dimension(record.getBenchmark(), record.getMetric()); + } - private static DimensionInfo dimRecordToDimInfo(DimensionRecord record) { - return new DimensionInfo( - dimRecordToDim(record), - new Unit(record.getUnit()), - Interpretation.fromTextualRepresentation(record.getInterpretation()), - record.getSignificant() - ); - } + private static DimensionInfo dimRecordToDimInfo(DimensionRecord record) { + return new DimensionInfo( + dimRecordToDim(record), + new Unit(record.getUnit()), + Interpretation.fromTextualRepresentation(record.getInterpretation()), + record.getSignificant()); + } - /** - * Convert a {@link DimensionInfo} to a JDBC record for the "dimension" table. - * - * @param info the dimension info to convert - * @return the newly created database record - */ - public static DimensionRecord dimInfoToDimRecord(DimensionInfo info) { - return new DimensionRecord( - info.getDimension().getBenchmark(), - info.getDimension().getMetric(), - info.getUnit().getName(), - info.getInterpretation().getTextualRepresentation(), - info.isSignificant() - ); - } + /** + * Convert a {@link DimensionInfo} to a JDBC record for the "dimension" table. + * + * @param info the dimension info to convert + * @return the newly created database record + */ + public static DimensionRecord dimInfoToDimRecord(DimensionInfo info) { + return new DimensionRecord( + info.getDimension().getBenchmark(), + info.getDimension().getMetric(), + info.getUnit().getName(), + info.getInterpretation().getTextualRepresentation(), + info.isSignificant()); + } - /** - * Check if a dimension exists. - * - * @param dimension the dimension whose existence to check - * @throws NoSuchDimensionException if the dimension doesn't exist - */ - public void guardDimensionExists(Dimension dimension) throws NoSuchDimensionException { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - db.dsl() - .selectFrom(DIMENSION) - .where(DIMENSION.BENCHMARK.eq(dimension.getBenchmark())) - .and(DIMENSION.METRIC.eq(dimension.getMetric())) - .fetchSingle(); - } catch (DataAccessException e) { - throw new NoSuchDimensionException(e, dimension); - } - } + /** + * Check if a dimension exists. + * + * @param dimension the dimension whose existence to check + * @throws NoSuchDimensionException if the dimension doesn't exist + */ + public void guardDimensionExists(Dimension dimension) throws NoSuchDimensionException { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + db.dsl() + .selectFrom(DIMENSION) + .where(DIMENSION.BENCHMARK.eq(dimension.getBenchmark())) + .and(DIMENSION.METRIC.eq(dimension.getMetric())) + .fetchSingle(); + } catch (DataAccessException e) { + throw new NoSuchDimensionException(e, dimension); + } + } - /** - * Get a dimension's info. Always returns a {@link DimensionInfo}, even if the dimension does not - * exist (in which case it uses the default values). - * - * @param dimension the dimension whose info to get - * @return that dimension's info (or the default info if the dimension doesn't exist) - */ - public DimensionInfo getDimensionInfo(Dimension dimension) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(DIMENSION) - .where(DIMENSION.BENCHMARK.eq(dimension.getBenchmark())) - .and(DIMENSION.METRIC.eq(dimension.getMetric())) - .fetchOptional() - .map(DimensionReadAccess::dimRecordToDimInfo) - .orElse(new DimensionInfo(dimension)); - } - } + /** + * Get a dimension's info. Always returns a {@link DimensionInfo}, even if the dimension does not + * exist (in which case it uses the default values). + * + * @param dimension the dimension whose info to get + * @return that dimension's info (or the default info if the dimension doesn't exist) + */ + public DimensionInfo getDimensionInfo(Dimension dimension) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .selectFrom(DIMENSION) + .where(DIMENSION.BENCHMARK.eq(dimension.getBenchmark())) + .and(DIMENSION.METRIC.eq(dimension.getMetric())) + .fetchOptional() + .map(DimensionReadAccess::dimRecordToDimInfo) + .orElse(new DimensionInfo(dimension)); + } + } - /** - * Get {@link DimensionInfo}s for multiple dimensions at the same time. Similar to {@link - * #getDimensionInfo(Dimension)}, this function will return a {@link DimensionInfo} for every - * dimension even if it doesn't exist. - * - * @param dimensions the dimensions whose infos to get - * @return an info for every input dimension. This set should always be the same size as the input - * set. - */ - public Set getDimensionInfos(Set dimensions) { - final Set infos; + /** + * Get {@link DimensionInfo}s for multiple dimensions at the same time. Similar to {@link + * #getDimensionInfo(Dimension)}, this function will return a {@link DimensionInfo} for every + * dimension even if it doesn't exist. + * + * @param dimensions the dimensions whose infos to get + * @return an info for every input dimension. This set should always be the same size as the input + * set. + */ + public Set getDimensionInfos(Set dimensions) { + final Set infos; - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - infos = db.dsl() - .selectFrom(DIMENSION) - .stream() // This is efficient enough, we don't have that many dimensions - .map(DimensionReadAccess::dimRecordToDimInfo) - .filter(info -> dimensions.contains(info.getDimension())) - .collect(Collectors.toCollection(HashSet::new)); - } + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + infos = + db + .dsl() + .selectFrom(DIMENSION) + .stream() // This is efficient enough, we don't have that many dimensions + .map(DimensionReadAccess::dimRecordToDimInfo) + .filter(info -> dimensions.contains(info.getDimension())) + .collect(Collectors.toCollection(HashSet::new)); + } - dimensions.forEach(dimension -> infos.add(new DimensionInfo(dimension))); + dimensions.forEach(dimension -> infos.add(new DimensionInfo(dimension))); - return infos; - } + return infos; + } - /** - * Get {@link DimensionInfo}s for multiple dimensions at the same time. Works the same as {@link - * #getDimensionInfos(Set)} but sorts the dimensions into a map for convenience. - * - * @param dimensions the dimensions whose infos to get - * @return an info for every input dimension. This map should always be the same size as the input - * set. - */ - public Map getDimensionInfoMap(Set dimensions) { - return getDimensionInfos(dimensions).stream() - .collect(toMap(DimensionInfo::getDimension, it -> it)); - } + /** + * Get {@link DimensionInfo}s for multiple dimensions at the same time. Works the same as {@link + * #getDimensionInfos(Set)} but sorts the dimensions into a map for convenience. + * + * @param dimensions the dimensions whose infos to get + * @return an info for every input dimension. This map should always be the same size as the input + * set. + */ + public Map getDimensionInfoMap(Set dimensions) { + return getDimensionInfos(dimensions).stream() + .collect(toMap(DimensionInfo::getDimension, it -> it)); + } - /** - * @return all currently known dimensions - */ - public Set getAllDimensions() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(DIMENSION) - .stream() - .map(DimensionReadAccess::dimRecordToDimInfo) - .collect(toSet()); - } - } + /** + * @return all currently known dimensions + */ + public Set getAllDimensions() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl().selectFrom(DIMENSION).stream() + .map(DimensionReadAccess::dimRecordToDimInfo) + .collect(toSet()); + } + } - public Set getSignificantDimensions() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(DIMENSION) - .where(DIMENSION.SIGNIFICANT) - .stream() - .map(DimensionReadAccess::dimRecordToDim) - .collect(toSet()); - } - } + public Set getSignificantDimensions() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl().selectFrom(DIMENSION).where(DIMENSION.SIGNIFICANT).stream() + .map(DimensionReadAccess::dimRecordToDim) + .collect(toSet()); + } + } - public Map getRunsPerDimension() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC, count()) - .from(MEASUREMENT) - .groupBy(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) - .fetchMap( - record -> new Dimension(record.value1(), record.value2()), - Record3::value3 - ); - } - } + public Map getRunsPerDimension() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC, count()) + .from(MEASUREMENT) + .groupBy(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) + .fetchMap(record -> new Dimension(record.value1(), record.value2()), Record3::value3); + } + } - public Map getUntrackedRunsPerDimension() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC, count()) - .from(MEASUREMENT) - .join(RUN) - .on(RUN.ID.eq(MEASUREMENT.RUN_ID)) - .join(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) - .where(not(KNOWN_COMMIT.TRACKED)) - .groupBy(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) - .fetchMap( - record -> new Dimension(record.value1(), record.value2()), - Record3::value3 - ); - } - } + public Map getUntrackedRunsPerDimension() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC, count()) + .from(MEASUREMENT) + .join(RUN) + .on(RUN.ID.eq(MEASUREMENT.RUN_ID)) + .join(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID).and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) + .where(not(KNOWN_COMMIT.TRACKED)) + .groupBy(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) + .fetchMap(record -> new Dimension(record.value1(), record.value2()), Record3::value3); + } + } - public Map getUnreachableRunsPerDimension() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .select(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC, count()) - .from(MEASUREMENT) - .join(RUN) - .on(RUN.ID.eq(MEASUREMENT.RUN_ID)) - .join(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID) - .and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) - .where(not(KNOWN_COMMIT.REACHABLE)) - .groupBy(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) - .fetchMap( - record -> new Dimension(record.value1(), record.value2()), - Record3::value3 - ); - } - } + public Map getUnreachableRunsPerDimension() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl() + .select(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC, count()) + .from(MEASUREMENT) + .join(RUN) + .on(RUN.ID.eq(MEASUREMENT.RUN_ID)) + .join(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(RUN.REPO_ID).and(KNOWN_COMMIT.HASH.eq(RUN.COMMIT_HASH))) + .where(not(KNOWN_COMMIT.REACHABLE)) + .groupBy(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) + .fetchMap(record -> new Dimension(record.value1(), record.value2()), Record3::value3); + } + } - /** - * Find a repo's available dimensions, i. e. the dimensions a repo has at least one measurement - * for. Only considers runs for commits, not runs for tars. - * - * @param repoId the repo's id - * @return the repo's available dimensions - */ - public Set getAvailableDimensions(RepoId repoId) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectDistinct(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) - .from(RUN) - .join(MEASUREMENT).on(MEASUREMENT.RUN_ID.eq(RUN.ID)) - .where(RUN.REPO_ID.eq(repoId.getIdAsString())) - .and(RUN.COMMIT_HASH.isNotNull()) - .stream() - .map(record -> new Dimension(record.value1(), record.value2())) - .collect(toSet()); - } - } + /** + * Find a repo's available dimensions, i. e. the dimensions a repo has at least one measurement + * for. Only considers runs for commits, not runs for tars. + * + * @param repoId the repo's id + * @return the repo's available dimensions + */ + public Set getAvailableDimensions(RepoId repoId) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db + .dsl() + .selectDistinct(MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) + .from(RUN) + .join(MEASUREMENT) + .on(MEASUREMENT.RUN_ID.eq(RUN.ID)) + .where(RUN.REPO_ID.eq(repoId.getIdAsString())) + .and(RUN.COMMIT_HASH.isNotNull()) + .stream() + .map(record -> new Dimension(record.value1(), record.value2())) + .collect(toSet()); + } + } - /** - * Find multiple repos' available dimensions, i. e. the dimensions a repo has at least one - * measurement for. - * - * @param repoIds the ids of the repos to return the available dimensions of - * @return the available dimensions for each input repo, including those that don't have any - * available dimensions. In other words, this is guaranteed to contain an entry for every input - * repo id. - */ - public Map> getAvailableDimensions(Collection repoIds) { - Set repoIdStrings = repoIds.stream() - .map(RepoId::getIdAsString) - .collect(toSet()); + /** + * Find multiple repos' available dimensions, i. e. the dimensions a repo has at least one + * measurement for. + * + * @param repoIds the ids of the repos to return the available dimensions of + * @return the available dimensions for each input repo, including those that don't have any + * available dimensions. In other words, this is guaranteed to contain an entry for every + * input repo id. + */ + public Map> getAvailableDimensions(Collection repoIds) { + Set repoIdStrings = repoIds.stream().map(RepoId::getIdAsString).collect(toSet()); - final HashMap> availableDimensions; + final HashMap> availableDimensions; - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - availableDimensions = db.dsl() - .selectDistinct(RUN.REPO_ID, MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) - .from(RUN) - .join(MEASUREMENT).on(MEASUREMENT.RUN_ID.eq(RUN.ID)) - .where(RUN.REPO_ID.in(repoIdStrings)) - .and(RUN.COMMIT_HASH.isNotNull()) - .stream() - .collect(Collectors.groupingBy( - record -> RepoId.fromString(record.value1()), - HashMap::new, - Collectors.mapping( - record -> new Dimension(record.value2(), record.value3()), - toSet() - ) - )); - } + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + availableDimensions = + db + .dsl() + .selectDistinct(RUN.REPO_ID, MEASUREMENT.BENCHMARK, MEASUREMENT.METRIC) + .from(RUN) + .join(MEASUREMENT) + .on(MEASUREMENT.RUN_ID.eq(RUN.ID)) + .where(RUN.REPO_ID.in(repoIdStrings)) + .and(RUN.COMMIT_HASH.isNotNull()) + .stream() + .collect( + Collectors.groupingBy( + record -> RepoId.fromString(record.value1()), + HashMap::new, + Collectors.mapping( + record -> new Dimension(record.value2(), record.value3()), toSet()))); + } - for (RepoId repoId : repoIds) { - availableDimensions.putIfAbsent(repoId, Set.of()); - } + for (RepoId repoId : repoIds) { + availableDimensions.putIfAbsent(repoId, Set.of()); + } - return availableDimensions; - } + return availableDimensions; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionWriteAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionWriteAccess.java index 1f6a84fb4..5fd3e717f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionWriteAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionWriteAccess.java @@ -11,43 +11,47 @@ public class DimensionWriteAccess extends DimensionReadAccess { - private final AvailableDimensionsCache availableDimensionsCache; - private final RunCache runCache; - - public DimensionWriteAccess(DatabaseStorage databaseStorage, - AvailableDimensionsCache availableDimensionsCache, RunCache runCache) { - - super(databaseStorage); - - this.availableDimensionsCache = availableDimensionsCache; - this.runCache = runCache; - } - - /** - * Deletes the given dimensions atomically. If any dimension does not exist, nothing is - * deleted and an exception is raised. - * - * @param dimensions the dimensions to delete - * @throws NoSuchDimensionException if a dimension does not exist - */ - public void deleteDimensions(Collection dimensions) { - databaseStorage.acquireWriteTransaction(db -> { - // This is not a good idea with most databases, but should be fine with SQLite as it is - // in-process. - for (Dimension dimension : dimensions) { - int affectedRows = db.dsl() - .deleteFrom(DIMENSION) - .where(DIMENSION.BENCHMARK.eq(dimension.getBenchmark())) - .and(DIMENSION.METRIC.eq(dimension.getMetric())) - .execute(); - - if (affectedRows == 0) { - throw new NoSuchDimensionException(dimension); - } - } - }); - - availableDimensionsCache.invalidateAll(); - runCache.invalidateAll(); - } + private final AvailableDimensionsCache availableDimensionsCache; + private final RunCache runCache; + + public DimensionWriteAccess( + DatabaseStorage databaseStorage, + AvailableDimensionsCache availableDimensionsCache, + RunCache runCache) { + + super(databaseStorage); + + this.availableDimensionsCache = availableDimensionsCache; + this.runCache = runCache; + } + + /** + * Deletes the given dimensions atomically. If any dimension does not exist, nothing is + * deleted and an exception is raised. + * + * @param dimensions the dimensions to delete + * @throws NoSuchDimensionException if a dimension does not exist + */ + public void deleteDimensions(Collection dimensions) { + databaseStorage.acquireWriteTransaction( + db -> { + // This is not a good idea with most databases, but should be fine with SQLite as it is + // in-process. + for (Dimension dimension : dimensions) { + int affectedRows = + db.dsl() + .deleteFrom(DIMENSION) + .where(DIMENSION.BENCHMARK.eq(dimension.getBenchmark())) + .and(DIMENSION.METRIC.eq(dimension.getMetric())) + .execute(); + + if (affectedRows == 0) { + throw new NoSuchDimensionException(dimension); + } + } + }); + + availableDimensionsCache.invalidateAll(); + runCache.invalidateAll(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Dimension.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Dimension.java index 44bebc52d..1fd22798f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Dimension.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Dimension.java @@ -9,52 +9,48 @@ */ public class Dimension implements Comparable { - private final String benchmark; - private final String metric; - - public Dimension(String benchmark, String metric) { - this.benchmark = Objects.requireNonNull(benchmark); - this.metric = Objects.requireNonNull(metric); - } - - public String getBenchmark() { - return benchmark; - } - - public String getMetric() { - return metric; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Dimension that = (Dimension) o; - return Objects.equals(benchmark, that.benchmark) && - Objects.equals(metric, that.metric); - } - - @Override - public int hashCode() { - return Objects.hash(benchmark, metric); - } - - @Override - public String toString() { - return "Dimension{" + - "benchmark='" + benchmark + '\'' + - ", metric='" + metric + '\'' + - '}'; - } - - @Override - public int compareTo(Dimension other) { - return Comparator.comparing(Dimension::getBenchmark) - .thenComparing(Dimension::getMetric) - .compare(this, other); - } + private final String benchmark; + private final String metric; + + public Dimension(String benchmark, String metric) { + this.benchmark = Objects.requireNonNull(benchmark); + this.metric = Objects.requireNonNull(metric); + } + + public String getBenchmark() { + return benchmark; + } + + public String getMetric() { + return metric; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Dimension that = (Dimension) o; + return Objects.equals(benchmark, that.benchmark) && Objects.equals(metric, that.metric); + } + + @Override + public int hashCode() { + return Objects.hash(benchmark, metric); + } + + @Override + public String toString() { + return "Dimension{" + "benchmark='" + benchmark + '\'' + ", metric='" + metric + '\'' + '}'; + } + + @Override + public int compareTo(Dimension other) { + return Comparator.comparing(Dimension::getBenchmark) + .thenComparing(Dimension::getMetric) + .compare(this, other); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfo.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfo.java index 3f9498b3a..3047182da 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfo.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfo.java @@ -4,86 +4,86 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * Information associated globally with a dimension. - */ +/** Information associated globally with a dimension. */ public class DimensionInfo { - private static final boolean DEFAULT_SIGNIFICANCE = true; - - private final Dimension dimension; - private final Unit unit; - private final Interpretation interpretation; - private final boolean significant; - - public DimensionInfo(Dimension dimension, Unit unit, Interpretation interpretation, - boolean significant) { - - this.dimension = Objects.requireNonNull(dimension); - this.unit = Objects.requireNonNull(unit); - this.interpretation = Objects.requireNonNull(interpretation); - this.significant = significant; - } - - public DimensionInfo(Dimension dimension, @Nullable Unit unit, - @Nullable Interpretation interpretation) { - - this( - dimension, - Optional.ofNullable(unit).orElse(Unit.DEFAULT), - Optional.ofNullable(interpretation).orElse(Interpretation.DEFAULT), - DEFAULT_SIGNIFICANCE - ); - } - - /** - * Create a {@link DimensionInfo} with default unit and interpretation. - * - * @param dimension the dimension - */ - public DimensionInfo(Dimension dimension) { - this(dimension, Unit.DEFAULT, Interpretation.DEFAULT, DEFAULT_SIGNIFICANCE); - } - - public Dimension getDimension() { - return dimension; - } - - public Unit getUnit() { - return unit; - } - - public Interpretation getInterpretation() { - return interpretation; - } - - public boolean isSignificant() { - return significant; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DimensionInfo dimensionInfo = (DimensionInfo) o; - return dimension.equals(dimensionInfo.dimension); - } - - @Override - public int hashCode() { - return Objects.hash(dimension); - } - - @Override - public String toString() { - return "DimensionInfo{" + - "dimension=" + dimension + - ", unit=" + unit + - ", interpretation=" + interpretation + - '}'; - } + private static final boolean DEFAULT_SIGNIFICANCE = true; + + private final Dimension dimension; + private final Unit unit; + private final Interpretation interpretation; + private final boolean significant; + + public DimensionInfo( + Dimension dimension, Unit unit, Interpretation interpretation, boolean significant) { + + this.dimension = Objects.requireNonNull(dimension); + this.unit = Objects.requireNonNull(unit); + this.interpretation = Objects.requireNonNull(interpretation); + this.significant = significant; + } + + public DimensionInfo( + Dimension dimension, @Nullable Unit unit, @Nullable Interpretation interpretation) { + + this( + dimension, + Optional.ofNullable(unit).orElse(Unit.DEFAULT), + Optional.ofNullable(interpretation).orElse(Interpretation.DEFAULT), + DEFAULT_SIGNIFICANCE); + } + + /** + * Create a {@link DimensionInfo} with default unit and interpretation. + * + * @param dimension the dimension + */ + public DimensionInfo(Dimension dimension) { + this(dimension, Unit.DEFAULT, Interpretation.DEFAULT, DEFAULT_SIGNIFICANCE); + } + + public Dimension getDimension() { + return dimension; + } + + public Unit getUnit() { + return unit; + } + + public Interpretation getInterpretation() { + return interpretation; + } + + public boolean isSignificant() { + return significant; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DimensionInfo dimensionInfo = (DimensionInfo) o; + return dimension.equals(dimensionInfo.dimension); + } + + @Override + public int hashCode() { + return Objects.hash(dimension); + } + + @Override + public String toString() { + return "DimensionInfo{" + + "dimension=" + + dimension + + ", unit=" + + unit + + ", interpretation=" + + interpretation + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Interpretation.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Interpretation.java index 1e226ac00..1e42c8d25 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Interpretation.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Interpretation.java @@ -3,75 +3,68 @@ import de.aaaaaaah.velcom.shared.protocol.serialization.Result; import java.util.List; -/** - * Describes how a measurement value should be interpreted. - */ +/** Describes how a measurement value should be interpreted. */ public enum Interpretation { - /** - * The measurement value can be better or worse compared to any other measurement value regardless - * of whether it is greater than or lesser than the other measurement value. - */ - NEUTRAL("NEUTRAL"), + /** + * The measurement value can be better or worse compared to any other measurement value regardless + * of whether it is greater than or lesser than the other measurement value. + */ + NEUTRAL("NEUTRAL"), - /** - * A measurement value is better than another if its value is lesser than the other's value. - */ - LESS_IS_BETTER("LESS_IS_BETTER"), + /** A measurement value is better than another if its value is lesser than the other's value. */ + LESS_IS_BETTER("LESS_IS_BETTER"), - /** - * A measurement value is better than another if its value is greater than the other's value. - */ - MORE_IS_BETTER("MORE_IS_BETTER"); + /** A measurement value is better than another if its value is greater than the other's value. */ + MORE_IS_BETTER("MORE_IS_BETTER"); - public static final Interpretation DEFAULT = NEUTRAL; + public static final Interpretation DEFAULT = NEUTRAL; - private final String textualRepresentation; + private final String textualRepresentation; - Interpretation(String textualRepresentation) { - this.textualRepresentation = textualRepresentation; - } + Interpretation(String textualRepresentation) { + this.textualRepresentation = textualRepresentation; + } - /** - * Try to interpret a string as the textual representation of an {@link Interpretation}. - * - * @param representation the string to interpret as {@link Interpretation} - * @return the resulting {@link Interpretation}, if any could be found - * @throws IllegalArgumentException if the string does not correspond to any {@link - * Interpretation}'s textual representation - */ - public static Interpretation fromTextualRepresentation(String representation) - throws IllegalArgumentException { + /** + * Try to interpret a string as the textual representation of an {@link Interpretation}. + * + * @param representation the string to interpret as {@link Interpretation} + * @return the resulting {@link Interpretation}, if any could be found + * @throws IllegalArgumentException if the string does not correspond to any {@link + * Interpretation}'s textual representation + */ + public static Interpretation fromTextualRepresentation(String representation) + throws IllegalArgumentException { - for (Interpretation interpretation : List.of(NEUTRAL, LESS_IS_BETTER, MORE_IS_BETTER)) { - if (interpretation.getTextualRepresentation().equals(representation)) { - return interpretation; - } - } + for (Interpretation interpretation : List.of(NEUTRAL, LESS_IS_BETTER, MORE_IS_BETTER)) { + if (interpretation.getTextualRepresentation().equals(representation)) { + return interpretation; + } + } - throw new IllegalArgumentException( - "\"" + representation + "\" is not a valid interpretation"); - } + throw new IllegalArgumentException("\"" + representation + "\" is not a valid interpretation"); + } - /** - * Converts a shared Interpretation to a backend one. - * - * @param interpretation the shared interpretation - * @return the backend interpretation - */ - public static Interpretation fromSharedRepresentation(Result.Interpretation interpretation) { - switch (interpretation) { - case NEUTRAL: - return NEUTRAL; - case LESS_IS_BETTER: - return LESS_IS_BETTER; - case MORE_IS_BETTER: - return MORE_IS_BETTER; - } - throw new IllegalArgumentException("Invalid interpretation"); - } + /** + * Converts a shared Interpretation to a backend one. + * + * @param interpretation the shared interpretation + * @return the backend interpretation + */ + public static Interpretation fromSharedRepresentation(Result.Interpretation interpretation) { + switch (interpretation) { + case NEUTRAL: + return NEUTRAL; + case LESS_IS_BETTER: + return LESS_IS_BETTER; + case MORE_IS_BETTER: + return MORE_IS_BETTER; + } + throw new IllegalArgumentException("Invalid interpretation"); + } - public String getTextualRepresentation() { - return textualRepresentation; - } + public String getTextualRepresentation() { + return textualRepresentation; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Unit.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Unit.java index 15fd7563c..b273ecad6 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Unit.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/Unit.java @@ -2,44 +2,40 @@ import java.util.Objects; -/** - * A measurement unit that measurement values are given in. - */ +/** A measurement unit that measurement values are given in. */ public class Unit { - public static final Unit DEFAULT = new Unit(""); - - public Unit(String name) { - this.name = Objects.requireNonNull(name); - } - - private final String name; - - public String getName() { - return name; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Unit unit = (Unit) o; - return name.equals(unit.name); - } - - @Override - public int hashCode() { - return Objects.hash(name); - } - - @Override - public String toString() { - return "Unit{" + - "name='" + name + '\'' + - '}'; - } + public static final Unit DEFAULT = new Unit(""); + + public Unit(String name) { + this.name = Objects.requireNonNull(name); + } + + private final String name; + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Unit unit = (Unit) o; + return name.equals(unit.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return "Unit{" + "name='" + name + '\'' + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/exceptions/NoSuchDimensionException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/exceptions/NoSuchDimensionException.java index 367f9b670..6b9c2130c 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/exceptions/NoSuchDimensionException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/exceptions/NoSuchDimensionException.java @@ -2,24 +2,22 @@ import de.aaaaaaah.velcom.backend.access.dimensionaccess.entities.Dimension; -/** - * This exception is thrown whenever an invalid {@link Dimension} is used. - */ +/** This exception is thrown whenever an invalid {@link Dimension} is used. */ public class NoSuchDimensionException extends RuntimeException { - private final Dimension invalidDimension; + private final Dimension invalidDimension; - public NoSuchDimensionException(Dimension invalidDimension) { - super("no dimension " + invalidDimension); - this.invalidDimension = invalidDimension; - } + public NoSuchDimensionException(Dimension invalidDimension) { + super("no dimension " + invalidDimension); + this.invalidDimension = invalidDimension; + } - public NoSuchDimensionException(Throwable t, Dimension invalidDimension) { - super("no dimension " + invalidDimension, t); - this.invalidDimension = invalidDimension; - } + public NoSuchDimensionException(Throwable t, Dimension invalidDimension) { + super("no dimension " + invalidDimension, t); + this.invalidDimension = invalidDimension; + } - public Dimension getInvalidDimension() { - return invalidDimension; - } + public Dimension getInvalidDimension() { + return invalidDimension; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccess.java index d61e50adf..67f350f52 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccess.java @@ -30,142 +30,131 @@ import org.jooq.codegen.db.tables.records.RepoRecord; import org.jooq.exception.DataAccessException; -/** - * Access for repos and their (tracked and untracked) branches. - */ +/** Access for repos and their (tracked and untracked) branches. */ public class RepoReadAccess { - protected final DatabaseStorage databaseStorage; - - public RepoReadAccess(DatabaseStorage databaseStorage) { - this.databaseStorage = databaseStorage; - } - - private static Repo repoRecordToRepo(RepoRecord record) { - return new Repo( - RepoId.fromString(record.getId()), - record.getName(), - new RemoteUrl(record.getRemoteUrl()), - record.getGithubAuthToken(), - record.getGithubCommentCutoff() - ); - } - - private static Branch branchRecordToBranch(BranchRecord record) { - return new Branch( - RepoId.fromString(record.getRepoId()), - BranchName.fromName(record.getName()), - new CommitHash(record.getLatestCommitHash()), - record.getTracked() - ); - } - - public List getAllRepos() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .fetch(REPO).stream() - .map(RepoReadAccess::repoRecordToRepo) - .collect(toList()); - } - } - - public Repo getRepo(RepoId repoId) throws NoSuchRepoException { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - RepoRecord repoRecord = db.dsl().fetchSingle(REPO, REPO.ID.eq(repoId.getIdAsString())); - return repoRecordToRepo(repoRecord); - } catch (DataAccessException e) { - throw new NoSuchRepoException(e, repoId); - } - } - - /** - * Check if a repo exists. - * - * @param repoId the id of the repo to look for - * @throws NoSuchRepoException if the repo doesn't exist - */ - public void guardRepoExists(RepoId repoId) throws NoSuchRepoException { - getRepo(repoId); - } - - /** - * Retrieve all of a repo's branches. - * - * @param repoId the id of the repo whose branches to return - * @return all of the repo's branches, or an empty list if the repo doesn't exist - */ - public List getAllBranches(RepoId repoId) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .fetch( - BRANCH, - BRANCH.REPO_ID.eq(repoId.getIdAsString()) - ) - .stream() - .map(RepoReadAccess::branchRecordToBranch) - .collect(toList()); - } - } - - public List searchBranches(int limit, @Nullable RepoId repoId, - String queryStr) { - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - var query = db.dsl() - .select( - BRANCH.REPO_ID, - BRANCH.NAME, - BRANCH.LATEST_COMMIT_HASH, - KNOWN_COMMIT.MESSAGE, - field(exists(select(one()) - .from(RUN) - .where(RUN.REPO_ID.eq(BRANCH.REPO_ID)) - .and(RUN.COMMIT_HASH.eq(BRANCH.LATEST_COMMIT_HASH)) - )) - ) - .from(BRANCH) - .join(KNOWN_COMMIT) - .on(KNOWN_COMMIT.REPO_ID.eq(BRANCH.REPO_ID)) - .and(KNOWN_COMMIT.HASH.eq(BRANCH.LATEST_COMMIT_HASH)) - .where(BRANCH.NAME.contains(queryStr)); - - if (repoId != null) { - query = query.and(BRANCH.REPO_ID.eq(repoId.getIdAsString())); - } - - return query.orderBy(KNOWN_COMMIT.COMMITTER_DATE.desc()) - .limit(limit) - .stream() - .map(record -> new SearchBranchDescription( - RepoId.fromString(record.value1()), - BranchName.fromName(record.value2()), - new CommitHash(record.value3()), - Commit.splitMessageIntoSections(record.value4()).getFirst(), - record.value5() - )) - .collect(toList()); - } - } - - /** - * Retrieve all of a repo's github commands. - */ - public List getCommands(RepoId repoId) { - return databaseStorage.acquireReadTransaction(db -> { - return db.dsl() - .selectFrom(GITHUB_COMMAND) - .where(GITHUB_COMMAND.REPO_ID.eq(repoId.getIdAsString())) - .stream() - .map(record -> new GithubCommand( - RepoId.fromString(record.getRepoId()), - record.getPr(), - BranchName.fromName(record.getTargetBranch()), - record.getComment(), - new CommitHash(record.getCommitHash()), - GithubCommandState.fromTextualRepresentation(record.getState()), - record.getTriesLeft() - )) - .collect(toList()); - }); - } + protected final DatabaseStorage databaseStorage; + + public RepoReadAccess(DatabaseStorage databaseStorage) { + this.databaseStorage = databaseStorage; + } + + private static Repo repoRecordToRepo(RepoRecord record) { + return new Repo( + RepoId.fromString(record.getId()), + record.getName(), + new RemoteUrl(record.getRemoteUrl()), + record.getGithubAuthToken(), + record.getGithubCommentCutoff()); + } + + private static Branch branchRecordToBranch(BranchRecord record) { + return new Branch( + RepoId.fromString(record.getRepoId()), + BranchName.fromName(record.getName()), + new CommitHash(record.getLatestCommitHash()), + record.getTracked()); + } + + public List getAllRepos() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl().fetch(REPO).stream().map(RepoReadAccess::repoRecordToRepo).collect(toList()); + } + } + + public Repo getRepo(RepoId repoId) throws NoSuchRepoException { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + RepoRecord repoRecord = db.dsl().fetchSingle(REPO, REPO.ID.eq(repoId.getIdAsString())); + return repoRecordToRepo(repoRecord); + } catch (DataAccessException e) { + throw new NoSuchRepoException(e, repoId); + } + } + + /** + * Check if a repo exists. + * + * @param repoId the id of the repo to look for + * @throws NoSuchRepoException if the repo doesn't exist + */ + public void guardRepoExists(RepoId repoId) throws NoSuchRepoException { + getRepo(repoId); + } + + /** + * Retrieve all of a repo's branches. + * + * @param repoId the id of the repo whose branches to return + * @return all of the repo's branches, or an empty list if the repo doesn't exist + */ + public List getAllBranches(RepoId repoId) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl().fetch(BRANCH, BRANCH.REPO_ID.eq(repoId.getIdAsString())).stream() + .map(RepoReadAccess::branchRecordToBranch) + .collect(toList()); + } + } + + public List searchBranches( + int limit, @Nullable RepoId repoId, String queryStr) { + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + var query = + db.dsl() + .select( + BRANCH.REPO_ID, + BRANCH.NAME, + BRANCH.LATEST_COMMIT_HASH, + KNOWN_COMMIT.MESSAGE, + field( + exists( + select(one()) + .from(RUN) + .where(RUN.REPO_ID.eq(BRANCH.REPO_ID)) + .and(RUN.COMMIT_HASH.eq(BRANCH.LATEST_COMMIT_HASH))))) + .from(BRANCH) + .join(KNOWN_COMMIT) + .on(KNOWN_COMMIT.REPO_ID.eq(BRANCH.REPO_ID)) + .and(KNOWN_COMMIT.HASH.eq(BRANCH.LATEST_COMMIT_HASH)) + .where(BRANCH.NAME.contains(queryStr)); + + if (repoId != null) { + query = query.and(BRANCH.REPO_ID.eq(repoId.getIdAsString())); + } + + return query.orderBy(KNOWN_COMMIT.COMMITTER_DATE.desc()).limit(limit).stream() + .map( + record -> + new SearchBranchDescription( + RepoId.fromString(record.value1()), + BranchName.fromName(record.value2()), + new CommitHash(record.value3()), + Commit.splitMessageIntoSections(record.value4()).getFirst(), + record.value5())) + .collect(toList()); + } + } + + /** Retrieve all of a repo's github commands. */ + public List getCommands(RepoId repoId) { + return databaseStorage.acquireReadTransaction( + db -> { + return db + .dsl() + .selectFrom(GITHUB_COMMAND) + .where(GITHUB_COMMAND.REPO_ID.eq(repoId.getIdAsString())) + .stream() + .map( + record -> + new GithubCommand( + RepoId.fromString(record.getRepoId()), + record.getPr(), + BranchName.fromName(record.getTargetBranch()), + record.getComment(), + new CommitHash(record.getCommitHash()), + GithubCommandState.fromTextualRepresentation(record.getState()), + record.getTriesLeft())) + .collect(toList()); + }); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccess.java index 94cc13325..5e66b7ec0 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccess.java @@ -24,158 +24,152 @@ import org.jooq.codegen.db.tables.records.RepoRecord; import org.jooq.exception.DataAccessException; -/** - * See {@link RepoReadAccess}. - */ +/** See {@link RepoReadAccess}. */ public class RepoWriteAccess extends RepoReadAccess { - private final AvailableDimensionsCache availableDimensionsCache; - private final RunCache runCache; - private final LatestRunCache latestRunCache; - - public RepoWriteAccess(DatabaseStorage databaseStorage, - AvailableDimensionsCache availableDimensionsCache, RunCache runCache, - LatestRunCache latestRunCache) { - - super(databaseStorage); - - this.availableDimensionsCache = availableDimensionsCache; - this.runCache = runCache; - this.latestRunCache = latestRunCache; - } - - /** - * Add a new repo to the database. Does not clone the repo. - * - * @param name the repo's name - * @param remoteUrl the repo's remote url - * @return the newly added repo - * @throws FailedToAddRepoException if the repo could not be added to the database - */ - public Repo addRepo(String name, RemoteUrl remoteUrl) throws FailedToAddRepoException { - UUID id = UUID.randomUUID(); - RepoRecord record = new RepoRecord(id.toString(), name, remoteUrl.getUrl(), null, null); - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl().batchInsert(record).execute(); - } catch (DataAccessException e) { - throw new FailedToAddRepoException(e, name, remoteUrl); - } - - return new Repo(new RepoId(id), name, remoteUrl, null, null); - } - - /** - * Delete an existing repo from the database. Does nothing if the repo is not in the database. - * - * @param repoId the repo's id - */ - public void deleteRepo(RepoId repoId) { - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .deleteFrom(REPO) - .where(REPO.ID.eq(repoId.getIdAsString())) - .execute(); - } - - availableDimensionsCache.invalidate(repoId); - - // No need to be more efficient about it since this doesn't happen very often anyways. - runCache.invalidateAll(); - - latestRunCache.invalidate(repoId); - } - - /** - * Change the name and/or remote url of a repo. Does nothing if no new name and no new remote url - * are given. - * - * @param repoId the repo's id - * @param name the new name (or null to keep the old name) - * @param remoteUrl the new remote url (or null to keep the old remote url) - */ - public void updateRepo(RepoId repoId, @Nullable String name, @Nullable RemoteUrl remoteUrl) { - // Not the nicest implementation, but simple to write and understand. - - if (name == null && remoteUrl == null) { - return; - } - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - if (name != null && remoteUrl != null) { - db.dsl() - .update(REPO) - .set(REPO.NAME, name) - .set(REPO.REMOTE_URL, remoteUrl.getUrl()) - .where(REPO.ID.eq(repoId.getIdAsString())) - .execute(); - } else if (name != null) { - db.dsl() - .update(REPO) - .set(REPO.NAME, name) - .where(REPO.ID.eq(repoId.getIdAsString())) - .execute(); - } else { - db.dsl() - .update(REPO) - .set(REPO.REMOTE_URL, remoteUrl.getUrl()) - .where(REPO.ID.eq(repoId.getIdAsString())) - .execute(); - } - } - } - - /** - * Sets which branches of this repo should be tracked. It may take a while for this change to take - * effect. - * - * @param repoId the id of the repo whose branches to set - * @param trackedBranches the new set of tracked branches - */ - public void setTrackedBranches(RepoId repoId, Collection trackedBranches) { - Set trackedNames = trackedBranches.stream() - .map(BranchName::getName) - .collect(Collectors.toSet()); - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .update(BRANCH) - .set(BRANCH.TRACKED, field(BRANCH.NAME.in(trackedNames))) - .where(BRANCH.REPO_ID.eq(repoId.getIdAsString())) - .execute(); - } - } - - /** - * Enable Github bot functionality by setting a GitHub auth token. - */ - public void setGithubAuthToken(RepoId repoId, String token) { - databaseStorage.acquireWriteTransaction(db -> { - db.dsl() - .update(REPO) - .set(REPO.GITHUB_AUTH_TOKEN, token) - .set(REPO.GITHUB_COMMENT_CUTOFF, Instant.now()) - .where(REPO.ID.eq(repoId.getIdAsString())) - .execute(); - }); - } - - /** - * Disable Github bot functionality by removing the GitHub auth token. - */ - public void unsetGithubAuthToken(RepoId repoId) { - databaseStorage.acquireWriteTransaction(db -> { - db.dsl() - .update(REPO) - .set(REPO.GITHUB_AUTH_TOKEN, (String) null) - .set(REPO.GITHUB_COMMENT_CUTOFF, (Instant) null) - .where(REPO.ID.eq(repoId.getIdAsString())) - .execute(); - - db.dsl() - .deleteFrom(GITHUB_COMMAND) - .where(GITHUB_COMMAND.REPO_ID.eq(repoId.getIdAsString())) - .execute(); - }); - } + private final AvailableDimensionsCache availableDimensionsCache; + private final RunCache runCache; + private final LatestRunCache latestRunCache; + + public RepoWriteAccess( + DatabaseStorage databaseStorage, + AvailableDimensionsCache availableDimensionsCache, + RunCache runCache, + LatestRunCache latestRunCache) { + + super(databaseStorage); + + this.availableDimensionsCache = availableDimensionsCache; + this.runCache = runCache; + this.latestRunCache = latestRunCache; + } + + /** + * Add a new repo to the database. Does not clone the repo. + * + * @param name the repo's name + * @param remoteUrl the repo's remote url + * @return the newly added repo + * @throws FailedToAddRepoException if the repo could not be added to the database + */ + public Repo addRepo(String name, RemoteUrl remoteUrl) throws FailedToAddRepoException { + UUID id = UUID.randomUUID(); + RepoRecord record = new RepoRecord(id.toString(), name, remoteUrl.getUrl(), null, null); + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().batchInsert(record).execute(); + } catch (DataAccessException e) { + throw new FailedToAddRepoException(e, name, remoteUrl); + } + + return new Repo(new RepoId(id), name, remoteUrl, null, null); + } + + /** + * Delete an existing repo from the database. Does nothing if the repo is not in the database. + * + * @param repoId the repo's id + */ + public void deleteRepo(RepoId repoId) { + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().deleteFrom(REPO).where(REPO.ID.eq(repoId.getIdAsString())).execute(); + } + + availableDimensionsCache.invalidate(repoId); + + // No need to be more efficient about it since this doesn't happen very often anyways. + runCache.invalidateAll(); + + latestRunCache.invalidate(repoId); + } + + /** + * Change the name and/or remote url of a repo. Does nothing if no new name and no new remote url + * are given. + * + * @param repoId the repo's id + * @param name the new name (or null to keep the old name) + * @param remoteUrl the new remote url (or null to keep the old remote url) + */ + public void updateRepo(RepoId repoId, @Nullable String name, @Nullable RemoteUrl remoteUrl) { + // Not the nicest implementation, but simple to write and understand. + + if (name == null && remoteUrl == null) { + return; + } + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + if (name != null && remoteUrl != null) { + db.dsl() + .update(REPO) + .set(REPO.NAME, name) + .set(REPO.REMOTE_URL, remoteUrl.getUrl()) + .where(REPO.ID.eq(repoId.getIdAsString())) + .execute(); + } else if (name != null) { + db.dsl() + .update(REPO) + .set(REPO.NAME, name) + .where(REPO.ID.eq(repoId.getIdAsString())) + .execute(); + } else { + db.dsl() + .update(REPO) + .set(REPO.REMOTE_URL, remoteUrl.getUrl()) + .where(REPO.ID.eq(repoId.getIdAsString())) + .execute(); + } + } + } + + /** + * Sets which branches of this repo should be tracked. It may take a while for this change to take + * effect. + * + * @param repoId the id of the repo whose branches to set + * @param trackedBranches the new set of tracked branches + */ + public void setTrackedBranches(RepoId repoId, Collection trackedBranches) { + Set trackedNames = + trackedBranches.stream().map(BranchName::getName).collect(Collectors.toSet()); + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl() + .update(BRANCH) + .set(BRANCH.TRACKED, field(BRANCH.NAME.in(trackedNames))) + .where(BRANCH.REPO_ID.eq(repoId.getIdAsString())) + .execute(); + } + } + + /** Enable Github bot functionality by setting a GitHub auth token. */ + public void setGithubAuthToken(RepoId repoId, String token) { + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .update(REPO) + .set(REPO.GITHUB_AUTH_TOKEN, token) + .set(REPO.GITHUB_COMMENT_CUTOFF, Instant.now()) + .where(REPO.ID.eq(repoId.getIdAsString())) + .execute(); + }); + } + + /** Disable Github bot functionality by removing the GitHub auth token. */ + public void unsetGithubAuthToken(RepoId repoId) { + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .update(REPO) + .set(REPO.GITHUB_AUTH_TOKEN, (String) null) + .set(REPO.GITHUB_COMMENT_CUTOFF, (Instant) null) + .where(REPO.ID.eq(repoId.getIdAsString())) + .execute(); + + db.dsl() + .deleteFrom(GITHUB_COMMAND) + .where(GITHUB_COMMAND.REPO_ID.eq(repoId.getIdAsString())) + .execute(); + }); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Branch.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Branch.java index d8ff7dcc7..1e99a7c14 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Branch.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Branch.java @@ -3,64 +3,65 @@ import de.aaaaaaah.velcom.backend.access.committaccess.entities.CommitHash; import java.util.Objects; -/** - * A branch of a git repo. - */ +/** A branch of a git repo. */ public class Branch { - private final RepoId repoId; - private final BranchName name; - private final CommitHash latestCommitHash; - private final boolean tracked; + private final RepoId repoId; + private final BranchName name; + private final CommitHash latestCommitHash; + private final boolean tracked; - public Branch(RepoId repoId, BranchName name, CommitHash latestCommitHash, boolean tracked) { - this.repoId = repoId; - this.name = name; - this.latestCommitHash = latestCommitHash; - this.tracked = tracked; - } + public Branch(RepoId repoId, BranchName name, CommitHash latestCommitHash, boolean tracked) { + this.repoId = repoId; + this.name = name; + this.latestCommitHash = latestCommitHash; + this.tracked = tracked; + } - public RepoId getRepoId() { - return repoId; - } + public RepoId getRepoId() { + return repoId; + } - public BranchName getName() { - return name; - } + public BranchName getName() { + return name; + } - public CommitHash getLatestCommitHash() { - return latestCommitHash; - } + public CommitHash getLatestCommitHash() { + return latestCommitHash; + } - public boolean isTracked() { - return tracked; - } + public boolean isTracked() { + return tracked; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Branch branch = (Branch) o; - return Objects.equals(repoId, branch.repoId) && - Objects.equals(name, branch.name); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Branch branch = (Branch) o; + return Objects.equals(repoId, branch.repoId) && Objects.equals(name, branch.name); + } - @Override - public int hashCode() { - return Objects.hash(repoId, name); - } + @Override + public int hashCode() { + return Objects.hash(repoId, name); + } - @Override - public String toString() { - return "Branch{" + - "repoId=" + repoId + - ", branchName=" + name + - ", latestCommitHash=" + latestCommitHash + - ", tracked=" + tracked + - '}'; - } + @Override + public String toString() { + return "Branch{" + + "repoId=" + + repoId + + ", branchName=" + + name + + ", latestCommitHash=" + + latestCommitHash + + ", tracked=" + + tracked + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/BranchName.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/BranchName.java index 6b56072ea..4c73976af 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/BranchName.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/BranchName.java @@ -2,77 +2,72 @@ import java.util.Objects; -/** - * The name of a branch. - */ +/** The name of a branch. */ public class BranchName implements Comparable { - private final String name; + private final String name; - private BranchName(String name) { - this.name = name; - } + private BranchName(String name) { + this.name = name; + } - /** - * Create a new {@link BranchName} from a string of the format {@code "refs/heads/[ranchname]"}. - * - * @param name a string of the format {@code "refs/heads/[ranchname]"} - * @return the new {@link BranchName} - */ - public static BranchName fromFullName(String name) { - return new BranchName(name); - } + /** + * Create a new {@link BranchName} from a string of the format {@code "refs/heads/[ranchname]"}. + * + * @param name a string of the format {@code "refs/heads/[ranchname]"} + * @return the new {@link BranchName} + */ + public static BranchName fromFullName(String name) { + return new BranchName(name); + } - /** - * Create a new {@link BranchName} from a string containing just the name of the branch. - * - * @param name a string containing just the name of the branch (i. e. {@code "master"}) - * @return the new {@link BranchName} - */ - public static BranchName fromName(String name) { - return new BranchName("refs/heads/" + name); - } + /** + * Create a new {@link BranchName} from a string containing just the name of the branch. + * + * @param name a string containing just the name of the branch (i. e. {@code "master"}) + * @return the new {@link BranchName} + */ + public static BranchName fromName(String name) { + return new BranchName("refs/heads/" + name); + } - public String getFullName() { - return name; - } + public String getFullName() { + return name; + } - public String getName() { - if (name.startsWith("refs/heads/")) { - return name.substring(11); - } else { - // Not sure what format the name is in - return name; - } - } + public String getName() { + if (name.startsWith("refs/heads/")) { + return name.substring(11); + } else { + // Not sure what format the name is in + return name; + } + } - @Override - public int compareTo(BranchName o) { - return name.compareTo(o.name); - } + @Override + public int compareTo(BranchName o) { + return name.compareTo(o.name); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - BranchName that = (BranchName) o; - return Objects.equals(name, that.name); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BranchName that = (BranchName) o; + return Objects.equals(name, that.name); + } - @Override - public int hashCode() { - return Objects.hash(name); - } - - @Override - public String toString() { - return "BranchName{" + - "name='" + name + '\'' + - '}'; - } + @Override + public int hashCode() { + return Objects.hash(name); + } + @Override + public String toString() { + return "BranchName{" + "name='" + name + '\'' + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RemoteUrl.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RemoteUrl.java index c3c57bf2c..30e11a6f9 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RemoteUrl.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RemoteUrl.java @@ -2,52 +2,47 @@ import java.util.Objects; -/** - * Locate a remote repository. - */ +/** Locate a remote repository. */ public class RemoteUrl { - private final String url; - - /** - * Construct a new remote url. - * - * @param url the url - */ - public RemoteUrl(String url) { - this.url = Objects.requireNonNull(url); - - if (url.isBlank()) { - throw new IllegalArgumentException("url must not be blank: " + url); - } - } - - public String getUrl() { - return url; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RemoteUrl remoteUrl = (RemoteUrl) o; - return url.equals(remoteUrl.url); - } - - @Override - public int hashCode() { - return Objects.hash(url); - } - - @Override - public String toString() { - return "RemoteUrl{" + - "url='" + url + '\'' + - '}'; - } - + private final String url; + + /** + * Construct a new remote url. + * + * @param url the url + */ + public RemoteUrl(String url) { + this.url = Objects.requireNonNull(url); + + if (url.isBlank()) { + throw new IllegalArgumentException("url must not be blank: " + url); + } + } + + public String getUrl() { + return url; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RemoteUrl remoteUrl = (RemoteUrl) o; + return url.equals(remoteUrl.url); + } + + @Override + public int hashCode() { + return Objects.hash(url); + } + + @Override + public String toString() { + return "RemoteUrl{" + "url='" + url + '\'' + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Repo.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Repo.java index 94f4c62aa..8f13b2334 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Repo.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/Repo.java @@ -8,154 +8,163 @@ import java.util.regex.Pattern; import javax.annotation.Nullable; -/** - * The information velcom knows about a repository it tracks. - */ +/** The information velcom knows about a repository it tracks. */ public class Repo { - private final RepoId id; - private final String name; - private final RemoteUrl remoteUrl; - @Nullable - private final String githubAccessToken; - @Nullable - private final Instant githubCommentCutoff; - - public Repo(RepoId id, String name, RemoteUrl remoteUrl, @Nullable String githubAccessToken, - @Nullable Instant githubCommentCutoff) { - - if ((githubAccessToken == null) != (githubCommentCutoff == null)) { - throw new IllegalArgumentException( - "githubAccessToken and githubCommentCutoff must either both be present or both be null"); - } - - this.id = id; - this.name = name; - this.remoteUrl = remoteUrl; - this.githubAccessToken = githubAccessToken; - this.githubCommentCutoff = githubCommentCutoff; - } - - public RepoId getId() { - return id; - } - - public UUID getIdAsUuid() { - return id.getId(); - } - - public String getIdAsString() { - return id.getIdAsString(); - } - - public String getName() { - return name; - } - - public RemoteUrl getRemoteUrl() { - return remoteUrl; - } - - public String getRemoteUrlAsString() { - return remoteUrl.getUrl(); - } - - public Optional getGithubRepoName() { - String remoteUrl = getRemoteUrlAsString(); - Pattern pattern = Pattern.compile("^(https://|git@)github.com[:/]([^/]+/[^/.]+)(\\.git)?$"); - Matcher matcher = pattern.matcher(remoteUrl); - if (matcher.find()) { - return Optional.of(matcher.group(2)); - } else { - return Optional.empty(); - } - } - - public Optional getGithubInfo() { - Optional repoName = getGithubRepoName(); - if (repoName.isPresent() && githubAccessToken != null && githubCommentCutoff != null) { - return Optional.of(new GithubInfo(repoName.get(), githubAccessToken, githubCommentCutoff)); - } else { - return Optional.empty(); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Repo repo = (Repo) o; - return Objects.equals(id, repo.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return "Repo{" + - "id=" + id.getIdAsString() + - ", name='" + name + '\'' + - ", remoteUrl='" + remoteUrl.getUrl() + '\'' + - ", githubCommentCutoff=" + githubCommentCutoff + - '}'; - } - - public static class GithubInfo { - - private final String repoName; - private final String accessToken; - private final Instant commentCutoff; - - public GithubInfo(String repoName, String accessToken, Instant commentCutoff) { - this.repoName = repoName; - this.accessToken = accessToken; - this.commentCutoff = commentCutoff; - } - - public String getRepoName() { - return repoName; - } - - public String getAccessToken() { - return accessToken; - } - - public Instant getCommentCutoff() { - return commentCutoff; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GithubInfo that = (GithubInfo) o; - return Objects.equals(repoName, that.repoName) && Objects - .equals(accessToken, that.accessToken) && Objects - .equals(commentCutoff, that.commentCutoff); - } - - @Override - public int hashCode() { - return Objects.hash(repoName, accessToken, commentCutoff); - } - - @Override - public String toString() { - return "GithubInfo{" + - "repoName='" + repoName + '\'' + - ", commentCutoff=" + commentCutoff + - '}'; - } - } + private final RepoId id; + private final String name; + private final RemoteUrl remoteUrl; + @Nullable private final String githubAccessToken; + @Nullable private final Instant githubCommentCutoff; + + public Repo( + RepoId id, + String name, + RemoteUrl remoteUrl, + @Nullable String githubAccessToken, + @Nullable Instant githubCommentCutoff) { + + if ((githubAccessToken == null) != (githubCommentCutoff == null)) { + throw new IllegalArgumentException( + "githubAccessToken and githubCommentCutoff must either both be present or both be null"); + } + + this.id = id; + this.name = name; + this.remoteUrl = remoteUrl; + this.githubAccessToken = githubAccessToken; + this.githubCommentCutoff = githubCommentCutoff; + } + + public RepoId getId() { + return id; + } + + public UUID getIdAsUuid() { + return id.getId(); + } + + public String getIdAsString() { + return id.getIdAsString(); + } + + public String getName() { + return name; + } + + public RemoteUrl getRemoteUrl() { + return remoteUrl; + } + + public String getRemoteUrlAsString() { + return remoteUrl.getUrl(); + } + + public Optional getGithubRepoName() { + String remoteUrl = getRemoteUrlAsString(); + Pattern pattern = Pattern.compile("^(https://|git@)github.com[:/]([^/]+/[^/.]+)(\\.git)?$"); + Matcher matcher = pattern.matcher(remoteUrl); + if (matcher.find()) { + return Optional.of(matcher.group(2)); + } else { + return Optional.empty(); + } + } + + public Optional getGithubInfo() { + Optional repoName = getGithubRepoName(); + if (repoName.isPresent() && githubAccessToken != null && githubCommentCutoff != null) { + return Optional.of(new GithubInfo(repoName.get(), githubAccessToken, githubCommentCutoff)); + } else { + return Optional.empty(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Repo repo = (Repo) o; + return Objects.equals(id, repo.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Repo{" + + "id=" + + id.getIdAsString() + + ", name='" + + name + + '\'' + + ", remoteUrl='" + + remoteUrl.getUrl() + + '\'' + + ", githubCommentCutoff=" + + githubCommentCutoff + + '}'; + } + + public static class GithubInfo { + + private final String repoName; + private final String accessToken; + private final Instant commentCutoff; + + public GithubInfo(String repoName, String accessToken, Instant commentCutoff) { + this.repoName = repoName; + this.accessToken = accessToken; + this.commentCutoff = commentCutoff; + } + + public String getRepoName() { + return repoName; + } + + public String getAccessToken() { + return accessToken; + } + + public Instant getCommentCutoff() { + return commentCutoff; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GithubInfo that = (GithubInfo) o; + return Objects.equals(repoName, that.repoName) + && Objects.equals(accessToken, that.accessToken) + && Objects.equals(commentCutoff, that.commentCutoff); + } + + @Override + public int hashCode() { + return Objects.hash(repoName, accessToken, commentCutoff); + } + + @Override + public String toString() { + return "GithubInfo{" + + "repoName='" + + repoName + + '\'' + + ", commentCutoff=" + + commentCutoff + + '}'; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoId.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoId.java index eb9017022..90b353053 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoId.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoId.java @@ -3,76 +3,69 @@ import java.util.Objects; import java.util.UUID; -/** - * A unique identifier for a repo. - */ +/** A unique identifier for a repo. */ public class RepoId implements Comparable { - private final UUID id; + private final UUID id; - public RepoId(UUID id) { - this.id = id; - } + public RepoId(UUID id) { + this.id = id; + } - /** - * Create a new, random {@link RepoId}. - */ - public RepoId() { - this(UUID.randomUUID()); - } + /** Create a new, random {@link RepoId}. */ + public RepoId() { + this(UUID.randomUUID()); + } - /** - * Create a new {@link RepoId} from a UUID string. - * - * @param string the UUID as string - * @return the repo id - */ - public static RepoId fromString(String string) { - return new RepoId(UUID.fromString(string)); - } + /** + * Create a new {@link RepoId} from a UUID string. + * + * @param string the UUID as string + * @return the repo id + */ + public static RepoId fromString(String string) { + return new RepoId(UUID.fromString(string)); + } - public String getDirectoryName() { - return getIdAsString(); - } + public String getDirectoryName() { + return getIdAsString(); + } - public UUID getId() { - return id; - } + public UUID getId() { + return id; + } - public String getIdAsString() { - return id.toString(); - } + public String getIdAsString() { + return id.toString(); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RepoId repoId = (RepoId) o; - return Objects.equals(id, repoId.id); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RepoId repoId = (RepoId) o; + return Objects.equals(id, repoId.id); + } - @Override - public int hashCode() { - return Objects.hash(id); - } + @Override + public int hashCode() { + return Objects.hash(id); + } - @Override - public String toString() { - return "RepoId{" + - "id=" + id + - '}'; - } - - @Override - public int compareTo(RepoId other) { - if (other == null) { - return 1; - } - return this.getId().toString().compareTo(other.getId().toString()); - } + @Override + public String toString() { + return "RepoId{" + "id=" + id + '}'; + } + @Override + public int compareTo(RepoId other) { + if (other == null) { + return 1; + } + return this.getId().toString().compareTo(other.getId().toString()); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/SearchBranchDescription.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/SearchBranchDescription.java index 21ff799ee..df36358e2 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/SearchBranchDescription.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/SearchBranchDescription.java @@ -5,69 +5,77 @@ public class SearchBranchDescription { - private final RepoId repoId; - private final BranchName name; - private final CommitHash commitHash; - private final String commitSummary; - private final boolean hasRun; + private final RepoId repoId; + private final BranchName name; + private final CommitHash commitHash; + private final String commitSummary; + private final boolean hasRun; - public SearchBranchDescription(RepoId repoId, BranchName name, CommitHash commitHash, - String commitSummary, boolean hasRun) { + public SearchBranchDescription( + RepoId repoId, BranchName name, CommitHash commitHash, String commitSummary, boolean hasRun) { - this.repoId = repoId; - this.name = name; - this.commitHash = commitHash; - this.commitSummary = commitSummary; - this.hasRun = hasRun; - } + this.repoId = repoId; + this.name = name; + this.commitHash = commitHash; + this.commitSummary = commitSummary; + this.hasRun = hasRun; + } - public RepoId getRepoId() { - return repoId; - } + public RepoId getRepoId() { + return repoId; + } - public BranchName getName() { - return name; - } + public BranchName getName() { + return name; + } - public CommitHash getCommitHash() { - return commitHash; - } + public CommitHash getCommitHash() { + return commitHash; + } - public String getCommitSummary() { - return commitSummary; - } + public String getCommitSummary() { + return commitSummary; + } - public boolean hasRun() { - return hasRun; - } + public boolean hasRun() { + return hasRun; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SearchBranchDescription that = (SearchBranchDescription) o; - return hasRun == that.hasRun && Objects.equals(repoId, that.repoId) && Objects - .equals(name, that.name) && Objects.equals(commitHash, that.commitHash) - && Objects.equals(commitSummary, that.commitSummary); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SearchBranchDescription that = (SearchBranchDescription) o; + return hasRun == that.hasRun + && Objects.equals(repoId, that.repoId) + && Objects.equals(name, that.name) + && Objects.equals(commitHash, that.commitHash) + && Objects.equals(commitSummary, that.commitSummary); + } - @Override - public int hashCode() { - return Objects.hash(repoId, name, commitHash, commitSummary, hasRun); - } + @Override + public int hashCode() { + return Objects.hash(repoId, name, commitHash, commitSummary, hasRun); + } - @Override - public String toString() { - return "SearchBranchDescription{" + - "repoId=" + repoId + - ", name=" + name + - ", commitHash=" + commitHash + - ", commitSummary='" + commitSummary + '\'' + - ", hasRun=" + hasRun + - '}'; - } + @Override + public String toString() { + return "SearchBranchDescription{" + + "repoId=" + + repoId + + ", name=" + + name + + ", commitHash=" + + commitHash + + ", commitSummary='" + + commitSummary + + '\'' + + ", hasRun=" + + hasRun + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/FailedToAddRepoException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/FailedToAddRepoException.java index 13a0a06ca..2025552c4 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/FailedToAddRepoException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/FailedToAddRepoException.java @@ -7,30 +7,29 @@ // Add this text to the javadoc below once the db schema has been updated: // This is most likely due to another repo with the same name already existing. -/** - * This exception is thrown when a repo could not be added into the db. - */ +/** This exception is thrown when a repo could not be added into the db. */ public class FailedToAddRepoException extends RuntimeException { - private final String name; - private final RemoteUrl remoteUrl; + private final String name; + private final RemoteUrl remoteUrl; - public FailedToAddRepoException(Throwable t, String name, RemoteUrl remoteUrl) { - super( - "failed to add repo named " + StringHelper.quote(name) - + " for remote url " + StringHelper.quote(remoteUrl.getUrl()), - t - ); + public FailedToAddRepoException(Throwable t, String name, RemoteUrl remoteUrl) { + super( + "failed to add repo named " + + StringHelper.quote(name) + + " for remote url " + + StringHelper.quote(remoteUrl.getUrl()), + t); - this.name = name; - this.remoteUrl = remoteUrl; - } + this.name = name; + this.remoteUrl = remoteUrl; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public RemoteUrl getRemoteUrl() { - return remoteUrl; - } + public RemoteUrl getRemoteUrl() { + return remoteUrl; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/NoSuchRepoException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/NoSuchRepoException.java index fe4a9f210..c7f5d7e17 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/NoSuchRepoException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/repoaccess/exceptions/NoSuchRepoException.java @@ -2,19 +2,17 @@ import de.aaaaaaah.velcom.backend.access.repoaccess.entities.RepoId; -/** - * This exception is thrown whenever an invalid {@link RepoId} is used. - */ +/** This exception is thrown whenever an invalid {@link RepoId} is used. */ public class NoSuchRepoException extends RuntimeException { - private final RepoId invalidId; + private final RepoId invalidId; - public NoSuchRepoException(Throwable t, RepoId invalidId) { - super("no repo with id " + invalidId, t); - this.invalidId = invalidId; - } + public NoSuchRepoException(Throwable t, RepoId invalidId) { + super("no repo with id " + invalidId, t); + this.invalidId = invalidId; + } - public RepoId getInvalidId() { - return invalidId; - } + public RepoId getInvalidId() { + return invalidId; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskReadAccess.java index b85a27145..54c049e97 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskReadAccess.java @@ -16,73 +16,69 @@ import org.jooq.codegen.db.tables.records.TaskRecord; import org.jooq.exception.DataAccessException; -/** - * Access for retrieving tasks and their status. - */ +/** Access for retrieving tasks and their status. */ public class TaskReadAccess { - protected final DatabaseStorage databaseStorage; - protected final TarFileStorage tarFileStorage; + protected final DatabaseStorage databaseStorage; + protected final TarFileStorage tarFileStorage; - public TaskReadAccess(DatabaseStorage databaseStorage, TarFileStorage tarFileStorage) { - this.databaseStorage = databaseStorage; - this.tarFileStorage = tarFileStorage; - } + public TaskReadAccess(DatabaseStorage databaseStorage, TarFileStorage tarFileStorage) { + this.databaseStorage = databaseStorage; + this.tarFileStorage = tarFileStorage; + } - protected static Task taskRecordToTask(TaskRecord record) { - return new Task( - TaskId.fromString(record.getId()), - record.getAuthor(), - TaskPriority.fromInt(record.getPriority()), - record.getInsertTime(), - record.getUpdateTime(), - AccessUtils.readSource(record.getRepoId(), record.getCommitHash(), record.getDescription()), - record.getInProcess() - ); - } + protected static Task taskRecordToTask(TaskRecord record) { + return new Task( + TaskId.fromString(record.getId()), + record.getAuthor(), + TaskPriority.fromInt(record.getPriority()), + record.getInsertTime(), + record.getUpdateTime(), + AccessUtils.readSource(record.getRepoId(), record.getCommitHash(), record.getDescription()), + record.getInProcess()); + } - /** - * Gets the task with the specified id. - * - * @param taskId the id of the task - * @return the task - * @throws NoSuchTaskException if no task with the specified id exists - */ - public Task getTask(TaskId taskId) throws NoSuchTaskException { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - TaskRecord record = db.dsl().fetchSingle(TASK, TASK.ID.eq(taskId.getIdAsString())); - return taskRecordToTask(record); - } catch (DataAccessException e) { - throw new NoSuchTaskException(e, taskId); - } - } + /** + * Gets the task with the specified id. + * + * @param taskId the id of the task + * @return the task + * @throws NoSuchTaskException if no task with the specified id exists + */ + public Task getTask(TaskId taskId) throws NoSuchTaskException { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + TaskRecord record = db.dsl().fetchSingle(TASK, TASK.ID.eq(taskId.getIdAsString())); + return taskRecordToTask(record); + } catch (DataAccessException e) { + throw new NoSuchTaskException(e, taskId); + } + } - /** - * A good way to check whether a task exists and to check its status if it exists. - * - * @param taskId the id of the task to check for - * @return the tasks status if the task exists, empty otherwise - */ - public Optional isTaskInProgress(TaskId taskId) { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - Boolean inProcess = db.dsl() - .selectFrom(TASK) - .where(TASK.ID.eq(taskId.getIdAsString())) - .fetchOne(TASK.IN_PROCESS); - return Optional.ofNullable(inProcess); - } - } + /** + * A good way to check whether a task exists and to check its status if it exists. + * + * @param taskId the id of the task to check for + * @return the tasks status if the task exists, empty otherwise + */ + public Optional isTaskInProgress(TaskId taskId) { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + Boolean inProcess = + db.dsl() + .selectFrom(TASK) + .where(TASK.ID.eq(taskId.getIdAsString())) + .fetchOne(TASK.IN_PROCESS); + return Optional.ofNullable(inProcess); + } + } - /** - * @return all tasks in no particular order - */ - public List getAllTasks() { - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - return db.dsl() - .selectFrom(TASK) - .stream() - .map(TaskReadAccess::taskRecordToTask) - .collect(Collectors.toList()); - } - } + /** + * @return all tasks in no particular order + */ + public List getAllTasks() { + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + return db.dsl().selectFrom(TASK).stream() + .map(TaskReadAccess::taskRecordToTask) + .collect(Collectors.toList()); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskWriteAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskWriteAccess.java index deb60a7a0..befb2c686 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskWriteAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/TaskWriteAccess.java @@ -32,285 +32,272 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Access for manipulating tasks. - */ +/** Access for manipulating tasks. */ public class TaskWriteAccess extends TaskReadAccess { - private static final Logger LOGGER = LoggerFactory.getLogger(TaskWriteAccess.class); - - public TaskWriteAccess(DatabaseStorage databaseStorage, TarFileStorage tarFileStorage) { - super(databaseStorage, tarFileStorage); - } - - private static TaskRecord taskToTaskRecord(Task task) { - return new TaskRecord( - task.getIdAsString(), - task.getAuthor(), - task.getPriority().asInt(), - task.getInsertTime(), - task.getUpdateTime(), - task.getRepoId().map(RepoId::getIdAsString).orElse(null), - task.getCommitHash().map(CommitHash::getHash).orElse(null), - task.getTarDescription().orElse(null), - task.isInProgress() - ); - } - - /** - * Insert a single commit if no corresponding task already exists and return the newly created - * task. - * - * @param author the new task's author - * @param priority the new task's priority - * @param repoId the commit's repo id - * @param hash the commit's hash - * @return the newly created task if no task corresponding to this commit already existed - */ - public Optional insertCommit(String author, TaskPriority priority, RepoId repoId, - CommitHash hash) { - - return databaseStorage.acquireWriteTransaction(db -> { - boolean taskAlreadyExists = db.dsl() - .selectFrom(TASK) - .where(TASK.REPO_ID.eq(repoId.getIdAsString())) - .and(TASK.COMMIT_HASH.eq(hash.getHash())) - .fetchAny() != null; - - if (taskAlreadyExists) { - return Optional.empty(); - } - - Task task = new Task(author, priority, Either.ofLeft(new CommitSource(repoId, hash))); - db.dsl().batchInsert(taskToTaskRecord(task)).execute(); - return Optional.of(task); - }); - } - - - /** - * Insert all commits for which no corresponding task exists yet. - * - * @param author the new task's author - * @param priority the new task's priority - * @param repoId the commit's repo id - * @param hashes the commit hashes - */ - public void insertCommits(String author, TaskPriority priority, RepoId repoId, - Collection hashes) { - - Set hashesAsStrings = hashes.stream() - .map(CommitHash::getHash) - .collect(Collectors.toSet()); - - databaseStorage.acquireWriteTransaction(db -> { - Set hashesAlreadyInQueue = db.dsl() - .selectFrom(TASK) - .where(TASK.REPO_ID.eq(repoId.getIdAsString())) - .and(TASK.COMMIT_HASH.in(hashesAsStrings)) - .fetchSet(TASK.COMMIT_HASH); - - List tasksToInsert = hashes.stream() - .filter(hash -> !hashesAlreadyInQueue.contains(hash.getHash())) - .map(hash -> new CommitSource(repoId, hash)) - .map(source -> new Task(author, priority, Either.ofLeft(source))) - .map(TaskWriteAccess::taskToTaskRecord) - .collect(Collectors.toList()); - - db.dsl().batchInsert(tasksToInsert).execute(); - }); - } - - /** - * Insert a tar file as a new task. - * - * @param author the new task's author - * @param priority the new task's priority - * @param description the tar file's description - * @param repoId the associated repo id, if any - * @param inputStream the tar file contents - * @return the newly created task - */ - public Task insertTar(String author, TaskPriority priority, String description, - @Nullable RepoId repoId, InputStream inputStream) throws TaskCreationException { - - TarSource source = new TarSource(description, repoId); - Task task = new Task(author, priority, Either.ofRight(source)); - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - TaskRecord record = taskToTaskRecord(task); - // I think this shouldn't throw an exception if it couldn't insert the task and just silently - // fail (failure signalled via return value), but I'm not entirely sure. - db.dsl().batchInsert(record).execute(); - } catch (DataAccessException e) { - // Probably a foreign key constraint indicating that the repo does not exist. - throw new TaskCreationException("Failed to create task, repo does not exist", false); - } - - try { - tarFileStorage.storeTarFile(task.getIdAsString(), inputStream); - } catch (IOException ignore) { - LOGGER.warn("Failed to store tar file for {}, task was not created", task); - throw new TaskCreationException("Failed to store tar file", true); - } - - return task; - } - - /** - * Atomically select a task to start and start it via a custom selector function. - * - * @param selector The selector is passed a list of all tasks with status "not in progress". The - * list is in no particular order. If the selector returns a task, that task will be started and - * an updated version of it returned. The task returned by the selector must be one of the - * input tasks! - * @return the task chosen by the selector, if it returned one - */ - public Optional startTask(Function, Optional> selector) { - return databaseStorage.acquireWriteTransaction(db -> { - List allTasks = db.dsl() - .selectFrom(TASK) - .where(not(TASK.IN_PROCESS)) - .stream() - .map(TaskReadAccess::taskRecordToTask) - .collect(Collectors.toList()); - - Set allTaskIds = allTasks.stream() - .map(Task::getId) - .collect(Collectors.toSet()); - - Optional taskToStart = selector.apply(allTasks); - - if (taskToStart.isEmpty()) { - return Optional.empty(); - } else if (!allTaskIds.contains(taskToStart.get())) { - // The selector is broken and has returned an invalid task - throw new NoSuchTaskException(taskToStart.get()); - } - - // The selector has returned a valid task - - String taskIdAsString = taskToStart.get().getIdAsString(); - db.dsl() - .update(TASK) - .set(TASK.IN_PROCESS, true) - .where(TASK.ID.eq(taskIdAsString)) - .execute(); - - TaskRecord record = db.dsl() - .selectFrom(TASK) - .where(TASK.ID.eq(taskIdAsString)) - .fetchOne(); - return Optional.of(taskRecordToTask(record)); - }); - } - - /** - * Delete the tasks with the specified ids. - * - * @param tasks the ids of the tasks to delete - */ - public void deleteTasks(Collection tasks) { - Set taskIds = tasks.stream() - .map(TaskId::getIdAsString) - .collect(Collectors.toSet()); - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .deleteFrom(TASK) - .where(TASK.ID.in(taskIds)) - .execute(); - } - - for (TaskId task : tasks) { - try { - tarFileStorage.removeTarFile(task.getIdAsString()); - } catch (IOException ignore) { - LOGGER.warn("Failed to remove tar file for {}", task); - // Not too bad since it will be cleaned up sooner or later anyways. - } - } - } - - /** - * Delete all tasks. - */ - public void deleteAllTasks() { - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl().deleteFrom(TASK).execute(); - } - - try { - tarFileStorage.removeAllFiles(); - } catch (IOException ignore) { - LOGGER.warn("Failed to remove all tar files"); - // Not too bad since they will be cleaned up sooner or later anyways. - } - } - - /** - * Set the priority of the task with the specified id, if such a task exists. Also changes the - * task's update time. - * - * @param taskId the task's id - * @param newPriority the task's new priority - */ - public void setTaskPriority(TaskId taskId, TaskPriority newPriority) { - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .update(TASK) - .set(TASK.PRIORITY, newPriority.asInt()) - .set(TASK.UPDATE_TIME, Instant.now()) - .where(TASK.ID.eq(taskId.getIdAsString())) - .execute(); - } - } - - /** - * Sets the status of the task with the specified id, if such a task exists. Does not changes the - * task's update time. - * - * @param taskId the task's id - * @param inProcess the task's new status - */ - public void setTaskInProgress(TaskId taskId, boolean inProcess) { - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .update(TASK) - .set(TASK.IN_PROCESS, inProcess) - .where(TASK.ID.eq(taskId.getIdAsString())) - .execute(); - } - } - - /** - * Reset all tasks to the "not in progress" status. - */ - public void resetAllTaskStatuses() { - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .update(TASK) - .set(TASK.IN_PROCESS, false) - .execute(); - } - } - - /** - * Remove all tar files that don't belong to a tar currently in the queue. - * - * @throws IOException if something went wrong while deleting the tar files - */ - public void cleanUpTarFiles() throws IOException { - Set tarTaskIds; - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - tarTaskIds = db.dsl() - .selectFrom(TASK) - .where(TASK.COMMIT_HASH.isNull()) - .stream() - .map(TaskRecord::getId) - .collect(Collectors.toSet()); - } - - tarFileStorage.removeUnknownFiles(tarTaskIds); - } + private static final Logger LOGGER = LoggerFactory.getLogger(TaskWriteAccess.class); + + public TaskWriteAccess(DatabaseStorage databaseStorage, TarFileStorage tarFileStorage) { + super(databaseStorage, tarFileStorage); + } + + private static TaskRecord taskToTaskRecord(Task task) { + return new TaskRecord( + task.getIdAsString(), + task.getAuthor(), + task.getPriority().asInt(), + task.getInsertTime(), + task.getUpdateTime(), + task.getRepoId().map(RepoId::getIdAsString).orElse(null), + task.getCommitHash().map(CommitHash::getHash).orElse(null), + task.getTarDescription().orElse(null), + task.isInProgress()); + } + + /** + * Insert a single commit if no corresponding task already exists and return the newly created + * task. + * + * @param author the new task's author + * @param priority the new task's priority + * @param repoId the commit's repo id + * @param hash the commit's hash + * @return the newly created task if no task corresponding to this commit already existed + */ + public Optional insertCommit( + String author, TaskPriority priority, RepoId repoId, CommitHash hash) { + + return databaseStorage.acquireWriteTransaction( + db -> { + boolean taskAlreadyExists = + db.dsl() + .selectFrom(TASK) + .where(TASK.REPO_ID.eq(repoId.getIdAsString())) + .and(TASK.COMMIT_HASH.eq(hash.getHash())) + .fetchAny() + != null; + + if (taskAlreadyExists) { + return Optional.empty(); + } + + Task task = new Task(author, priority, Either.ofLeft(new CommitSource(repoId, hash))); + db.dsl().batchInsert(taskToTaskRecord(task)).execute(); + return Optional.of(task); + }); + } + + /** + * Insert all commits for which no corresponding task exists yet. + * + * @param author the new task's author + * @param priority the new task's priority + * @param repoId the commit's repo id + * @param hashes the commit hashes + */ + public void insertCommits( + String author, TaskPriority priority, RepoId repoId, Collection hashes) { + + Set hashesAsStrings = + hashes.stream().map(CommitHash::getHash).collect(Collectors.toSet()); + + databaseStorage.acquireWriteTransaction( + db -> { + Set hashesAlreadyInQueue = + db.dsl() + .selectFrom(TASK) + .where(TASK.REPO_ID.eq(repoId.getIdAsString())) + .and(TASK.COMMIT_HASH.in(hashesAsStrings)) + .fetchSet(TASK.COMMIT_HASH); + + List tasksToInsert = + hashes.stream() + .filter(hash -> !hashesAlreadyInQueue.contains(hash.getHash())) + .map(hash -> new CommitSource(repoId, hash)) + .map(source -> new Task(author, priority, Either.ofLeft(source))) + .map(TaskWriteAccess::taskToTaskRecord) + .collect(Collectors.toList()); + + db.dsl().batchInsert(tasksToInsert).execute(); + }); + } + + /** + * Insert a tar file as a new task. + * + * @param author the new task's author + * @param priority the new task's priority + * @param description the tar file's description + * @param repoId the associated repo id, if any + * @param inputStream the tar file contents + * @return the newly created task + */ + public Task insertTar( + String author, + TaskPriority priority, + String description, + @Nullable RepoId repoId, + InputStream inputStream) + throws TaskCreationException { + + TarSource source = new TarSource(description, repoId); + Task task = new Task(author, priority, Either.ofRight(source)); + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + TaskRecord record = taskToTaskRecord(task); + // I think this shouldn't throw an exception if it couldn't insert the task and just silently + // fail (failure signalled via return value), but I'm not entirely sure. + db.dsl().batchInsert(record).execute(); + } catch (DataAccessException e) { + // Probably a foreign key constraint indicating that the repo does not exist. + throw new TaskCreationException("Failed to create task, repo does not exist", false); + } + + try { + tarFileStorage.storeTarFile(task.getIdAsString(), inputStream); + } catch (IOException ignore) { + LOGGER.warn("Failed to store tar file for {}, task was not created", task); + throw new TaskCreationException("Failed to store tar file", true); + } + + return task; + } + + /** + * Atomically select a task to start and start it via a custom selector function. + * + * @param selector The selector is passed a list of all tasks with status "not in progress". The + * list is in no particular order. If the selector returns a task, that task will be started + * and an updated version of it returned. The task returned by the selector must be one of + * the input tasks! + * @return the task chosen by the selector, if it returned one + */ + public Optional startTask(Function, Optional> selector) { + return databaseStorage.acquireWriteTransaction( + db -> { + List allTasks = + db.dsl().selectFrom(TASK).where(not(TASK.IN_PROCESS)).stream() + .map(TaskReadAccess::taskRecordToTask) + .collect(Collectors.toList()); + + Set allTaskIds = allTasks.stream().map(Task::getId).collect(Collectors.toSet()); + + Optional taskToStart = selector.apply(allTasks); + + if (taskToStart.isEmpty()) { + return Optional.empty(); + } else if (!allTaskIds.contains(taskToStart.get())) { + // The selector is broken and has returned an invalid task + throw new NoSuchTaskException(taskToStart.get()); + } + + // The selector has returned a valid task + + String taskIdAsString = taskToStart.get().getIdAsString(); + db.dsl() + .update(TASK) + .set(TASK.IN_PROCESS, true) + .where(TASK.ID.eq(taskIdAsString)) + .execute(); + + TaskRecord record = + db.dsl().selectFrom(TASK).where(TASK.ID.eq(taskIdAsString)).fetchOne(); + return Optional.of(taskRecordToTask(record)); + }); + } + + /** + * Delete the tasks with the specified ids. + * + * @param tasks the ids of the tasks to delete + */ + public void deleteTasks(Collection tasks) { + Set taskIds = tasks.stream().map(TaskId::getIdAsString).collect(Collectors.toSet()); + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().deleteFrom(TASK).where(TASK.ID.in(taskIds)).execute(); + } + + for (TaskId task : tasks) { + try { + tarFileStorage.removeTarFile(task.getIdAsString()); + } catch (IOException ignore) { + LOGGER.warn("Failed to remove tar file for {}", task); + // Not too bad since it will be cleaned up sooner or later anyways. + } + } + } + + /** Delete all tasks. */ + public void deleteAllTasks() { + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().deleteFrom(TASK).execute(); + } + + try { + tarFileStorage.removeAllFiles(); + } catch (IOException ignore) { + LOGGER.warn("Failed to remove all tar files"); + // Not too bad since they will be cleaned up sooner or later anyways. + } + } + + /** + * Set the priority of the task with the specified id, if such a task exists. Also changes the + * task's update time. + * + * @param taskId the task's id + * @param newPriority the task's new priority + */ + public void setTaskPriority(TaskId taskId, TaskPriority newPriority) { + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl() + .update(TASK) + .set(TASK.PRIORITY, newPriority.asInt()) + .set(TASK.UPDATE_TIME, Instant.now()) + .where(TASK.ID.eq(taskId.getIdAsString())) + .execute(); + } + } + + /** + * Sets the status of the task with the specified id, if such a task exists. Does not changes the + * task's update time. + * + * @param taskId the task's id + * @param inProcess the task's new status + */ + public void setTaskInProgress(TaskId taskId, boolean inProcess) { + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl() + .update(TASK) + .set(TASK.IN_PROCESS, inProcess) + .where(TASK.ID.eq(taskId.getIdAsString())) + .execute(); + } + } + + /** Reset all tasks to the "not in progress" status. */ + public void resetAllTaskStatuses() { + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().update(TASK).set(TASK.IN_PROCESS, false).execute(); + } + } + + /** + * Remove all tar files that don't belong to a tar currently in the queue. + * + * @throws IOException if something went wrong while deleting the tar files + */ + public void cleanUpTarFiles() throws IOException { + Set tarTaskIds; + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + tarTaskIds = + db.dsl().selectFrom(TASK).where(TASK.COMMIT_HASH.isNull()).stream() + .map(TaskRecord::getId) + .collect(Collectors.toSet()); + } + + tarFileStorage.removeUnknownFiles(tarTaskIds); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/Task.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/Task.java index 9b7e78f49..32edbf660 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/Task.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/Task.java @@ -10,117 +10,130 @@ import java.util.Optional; import java.util.UUID; -/** - * A task is an item in the queue. When it is benchmarked by a runner, it turns into a run. - */ +/** A task is an item in the queue. When it is benchmarked by a runner, it turns into a run. */ public class Task { - private final TaskId id; - private final String author; - private final TaskPriority priority; - private final Instant insertTime; - private final Instant updateTime; - private final Either source; - private final boolean inProgress; - // TODO: 07.11.20 Rename "in_process" to "in_progress" in db - - public Task(TaskId id, String author, TaskPriority priority, Instant insertTime, - Instant updateTime, Either source, boolean inProgress) { - - this.id = id; - this.author = author; - this.priority = priority; - this.insertTime = insertTime; - this.updateTime = updateTime; - this.source = source; - this.inProgress = inProgress; - } - - public Task(String author, TaskPriority priority, Either source) { - this.id = new TaskId(); - this.author = author; - this.priority = priority; - this.insertTime = Instant.now(); - this.updateTime = this.insertTime; - this.source = source; - this.inProgress = false; - } - - public TaskId getId() { - return id; - } - - public UUID getIdAsUuid() { - return id.getId(); - } - - public String getIdAsString() { - return id.getIdAsString(); - } - - public String getAuthor() { - return author; - } - - public TaskPriority getPriority() { - return priority; - } - - public Instant getInsertTime() { - return insertTime; - } - - public Instant getUpdateTime() { - return updateTime; - } - - public Either getSource() { - return source; - } - - public Optional getRepoId() { - return source.consume(it -> Optional.of(it.getRepoId()), TarSource::getRepoId); - } - - public Optional getCommitHash() { - return source.consume(it -> Optional.of(it.getHash()), it -> Optional.empty()); - } - - public Optional getTarDescription() { - return source.consume(it -> Optional.empty(), it -> Optional.of(it.getDescription())); - } - - public boolean isInProgress() { - return inProgress; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Task task = (Task) o; - return Objects.equals(id, task.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return "Task{" + - "id=" + id + - ", author='" + author + '\'' + - ", priority=" + priority + - ", insertTime=" + insertTime + - ", updateTime=" + updateTime + - ", source=" + source + - ", inProgress=" + inProgress + - '}'; - } + private final TaskId id; + private final String author; + private final TaskPriority priority; + private final Instant insertTime; + private final Instant updateTime; + private final Either source; + private final boolean inProgress; + + // TODO: 07.11.20 Rename "in_process" to "in_progress" in db + + public Task( + TaskId id, + String author, + TaskPriority priority, + Instant insertTime, + Instant updateTime, + Either source, + boolean inProgress) { + + this.id = id; + this.author = author; + this.priority = priority; + this.insertTime = insertTime; + this.updateTime = updateTime; + this.source = source; + this.inProgress = inProgress; + } + + public Task(String author, TaskPriority priority, Either source) { + this.id = new TaskId(); + this.author = author; + this.priority = priority; + this.insertTime = Instant.now(); + this.updateTime = this.insertTime; + this.source = source; + this.inProgress = false; + } + + public TaskId getId() { + return id; + } + + public UUID getIdAsUuid() { + return id.getId(); + } + + public String getIdAsString() { + return id.getIdAsString(); + } + + public String getAuthor() { + return author; + } + + public TaskPriority getPriority() { + return priority; + } + + public Instant getInsertTime() { + return insertTime; + } + + public Instant getUpdateTime() { + return updateTime; + } + + public Either getSource() { + return source; + } + + public Optional getRepoId() { + return source.consume(it -> Optional.of(it.getRepoId()), TarSource::getRepoId); + } + + public Optional getCommitHash() { + return source.consume(it -> Optional.of(it.getHash()), it -> Optional.empty()); + } + + public Optional getTarDescription() { + return source.consume(it -> Optional.empty(), it -> Optional.of(it.getDescription())); + } + + public boolean isInProgress() { + return inProgress; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Task task = (Task) o; + return Objects.equals(id, task.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Task{" + + "id=" + + id + + ", author='" + + author + + '\'' + + ", priority=" + + priority + + ", insertTime=" + + insertTime + + ", updateTime=" + + updateTime + + ", source=" + + source + + ", inProgress=" + + inProgress + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskId.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskId.java index fbde15848..d5be6076b 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskId.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskId.java @@ -4,73 +4,66 @@ import java.util.Objects; import java.util.UUID; -/** - * A unique identifier for a {@link Task}. - */ +/** A unique identifier for a {@link Task}. */ public class TaskId { - private final UUID id; + private final UUID id; - public TaskId(UUID id) { - this.id = Objects.requireNonNull(id); - } + public TaskId(UUID id) { + this.id = Objects.requireNonNull(id); + } - /** - * Create a new, random {@link TaskId}. - */ - public TaskId() { - this(UUID.randomUUID()); - } + /** Create a new, random {@link TaskId}. */ + public TaskId() { + this(UUID.randomUUID()); + } - /** - * Create a new {@link TaskId} from a UUID string. - * - * @param string the UUID as string - * @return the task id - */ - public static TaskId fromString(String string) { - return new TaskId(UUID.fromString(string)); - } + /** + * Create a new {@link TaskId} from a UUID string. + * + * @param string the UUID as string + * @return the task id + */ + public static TaskId fromString(String string) { + return new TaskId(UUID.fromString(string)); + } - public UUID getId() { - return id; - } + public UUID getId() { + return id; + } - public String getIdAsString() { - return id.toString(); - } + public String getIdAsString() { + return id.toString(); + } - /** - * Convert a task id to a run id. - * - * @return a run id with the same UUID - */ - public RunId toRunId() { - return new RunId(id); - } + /** + * Convert a task id to a run id. + * + * @return a run id with the same UUID + */ + public RunId toRunId() { + return new RunId(id); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - TaskId repoId = (TaskId) o; - return Objects.equals(id, repoId.id); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TaskId repoId = (TaskId) o; + return Objects.equals(id, repoId.id); + } - @Override - public int hashCode() { - return Objects.hash(id); - } - - @Override - public String toString() { - return "TaskId{" + - "id=" + id + - '}'; - } + @Override + public int hashCode() { + return Objects.hash(id); + } + @Override + public String toString() { + return "TaskId{" + "id=" + id + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskPriority.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskPriority.java index 7caf30868..9095f01c5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskPriority.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/entities/TaskPriority.java @@ -1,52 +1,49 @@ package de.aaaaaaah.velcom.backend.access.taskaccess.entities; -/** - * The priority a task can have in the queue. Lower means more important. - */ +/** The priority a task can have in the queue. Lower means more important. */ public enum TaskPriority { - /** - * The task has been manually prioritized. In the case of commits, manually adding them to the - * queue automatically prioritizes them. - */ - MANUAL(0), - /** - * The task has been created by a user (e. g. from an uploaded tar file or a GitHub command), but - * should not have priority over manually prioritized tasks. - */ - USER_CREATED(1), - /** - * The task represents a commit that has been automatically added to the queue by the listener. - */ - LISTENER(2); + /** + * The task has been manually prioritized. In the case of commits, manually adding them to the + * queue automatically prioritizes them. + */ + MANUAL(0), + /** + * The task has been created by a user (e. g. from an uploaded tar file or a GitHub command), but + * should not have priority over manually prioritized tasks. + */ + USER_CREATED(1), + /** + * The task represents a commit that has been automatically added to the queue by the listener. + */ + LISTENER(2); - private final int priority; + private final int priority; - TaskPriority(int priority) { - this.priority = priority; - } + TaskPriority(int priority) { + this.priority = priority; + } - /** - * Convert an int to a {@link TaskPriority}. Integers that don't correspond to a particular - * priority are interpreted as the closest available priority. - * - * @param priority the int to convert - * @return the matching priority - */ - public static TaskPriority fromInt(int priority) { - if (priority <= 0) { - return MANUAL; - } else if (priority == 1) { - return USER_CREATED; - } else { - return LISTENER; - } - } - - /** - * @return the priority as an integer - */ - public int asInt() { - return priority; - } + /** + * Convert an int to a {@link TaskPriority}. Integers that don't correspond to a particular + * priority are interpreted as the closest available priority. + * + * @param priority the int to convert + * @return the matching priority + */ + public static TaskPriority fromInt(int priority) { + if (priority <= 0) { + return MANUAL; + } else if (priority == 1) { + return USER_CREATED; + } else { + return LISTENER; + } + } + /** + * @return the priority as an integer + */ + public int asInt() { + return priority; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/NoSuchTaskException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/NoSuchTaskException.java index e09a5fe20..70834e187 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/NoSuchTaskException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/NoSuchTaskException.java @@ -2,24 +2,22 @@ import de.aaaaaaah.velcom.backend.access.taskaccess.entities.TaskId; -/** - * This exception is thrown whenever an invalid {@link TaskId} is used. - */ +/** This exception is thrown whenever an invalid {@link TaskId} is used. */ public class NoSuchTaskException extends RuntimeException { - private final TaskId invalidId; + private final TaskId invalidId; - public NoSuchTaskException(TaskId invalidId) { - super("no task with id " + invalidId); - this.invalidId = invalidId; - } + public NoSuchTaskException(TaskId invalidId) { + super("no task with id " + invalidId); + this.invalidId = invalidId; + } - public NoSuchTaskException(Throwable t, TaskId invalidId) { - super("no task with id " + invalidId, t); - this.invalidId = invalidId; - } + public NoSuchTaskException(Throwable t, TaskId invalidId) { + super("no task with id " + invalidId, t); + this.invalidId = invalidId; + } - public TaskId getInvalidId() { - return invalidId; - } + public TaskId getInvalidId() { + return invalidId; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/TaskCreationException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/TaskCreationException.java index 9a4e82730..4762ffc41 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/TaskCreationException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/access/taskaccess/exceptions/TaskCreationException.java @@ -2,15 +2,15 @@ public class TaskCreationException extends Exception { - private final boolean ourFault; + private final boolean ourFault; - public TaskCreationException(String message, boolean ourFault) { - super(message); + public TaskCreationException(String message, boolean ourFault) { + super(message); - this.ourFault = ourFault; - } + this.ourFault = ourFault; + } - public boolean isOurFault() { - return ourFault; - } + public boolean isOurFault() { + return ourFault; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/benchrepo/BenchRepo.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/benchrepo/BenchRepo.java index 665811d02..a049a787d 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/benchrepo/BenchRepo.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/benchrepo/BenchRepo.java @@ -8,50 +8,48 @@ import java.util.Objects; import java.util.Optional; -/** - * Represents the benchmark repository which is used to run various benchmarks on tasks. - */ +/** Represents the benchmark repository which is used to run various benchmarks on tasks. */ public class BenchRepo { - private final ArchiveReadAccess archiveAccess; - - public BenchRepo(ArchiveReadAccess archiveAccess) { - this.archiveAccess = Objects.requireNonNull(archiveAccess); - } - - /** - * @return the name of the bench repo's directory. - */ - public String getDirName() { - return archiveAccess.getBenchRepoDirName(); - } - - /** - * @return the bench repo's remote url - */ - public String getRemoteUrl() { - return archiveAccess.getBenchRepoRemoteUrl(); - } - - /** - * @return the current commit hash of the benchmark repository. - */ - public Optional getCurrentHash() { - return archiveAccess.getBenchRepoCommitHash(); - } - - /** - * Transfers the benchmark repository to the given {@link OutputStream}. - * - *

Note that the provided output stream will be closed after the transfer operation is done. - * - * @param outputStream the output stream - * @throws TarRetrieveException if the tar file could not be retrieved - * @throws TarTransferException if the tar file could not be transferred - */ - public void transfer(OutputStream outputStream) - throws TarRetrieveException, TarTransferException { - - archiveAccess.transferBenchRepo(Objects.requireNonNull(outputStream)); - } + private final ArchiveReadAccess archiveAccess; + + public BenchRepo(ArchiveReadAccess archiveAccess) { + this.archiveAccess = Objects.requireNonNull(archiveAccess); + } + + /** + * @return the name of the bench repo's directory. + */ + public String getDirName() { + return archiveAccess.getBenchRepoDirName(); + } + + /** + * @return the bench repo's remote url + */ + public String getRemoteUrl() { + return archiveAccess.getBenchRepoRemoteUrl(); + } + + /** + * @return the current commit hash of the benchmark repository. + */ + public Optional getCurrentHash() { + return archiveAccess.getBenchRepoCommitHash(); + } + + /** + * Transfers the benchmark repository to the given {@link OutputStream}. + * + *

Note that the provided output stream will be closed after the transfer operation is done. + * + * @param outputStream the output stream + * @throws TarRetrieveException if the tar file could not be retrieved + * @throws TarTransferException if the tar file could not be transferred + */ + public void transfer(OutputStream outputStream) + throws TarRetrieveException, TarTransferException { + + archiveAccess.transferBenchRepo(Objects.requireNonNull(outputStream)); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Policy.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Policy.java index 521cc16a0..7b6636d09 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Policy.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Policy.java @@ -27,108 +27,112 @@ */ class Policy { - private final Queue manualTasks; - private final Queue tarTasks; - - private final Map> listenerTasksPerRepo; - private final Queue repos; - - public Policy(List tasks, @Nullable RepoId currentRepoId) { - // Tasks ordered from newest to oldest (FILO) - List tasksInOrder = new ArrayList<>(tasks); - tasksInOrder.sort(comparing(Task::getUpdateTime)); - Collections.reverse(tasksInOrder); - - manualTasks = tasksInOrder.stream() - .filter(task -> task.getPriority().equals(TaskPriority.MANUAL)) - .collect(toCollection(ArrayDeque::new)); - - // Tar tasks need to be reversed since they need a FIFO order, not FILO - ArrayList tarTasksTmp = tasksInOrder.stream() - .filter(task -> task.getPriority().equals(TaskPriority.USER_CREATED)) - .collect(toCollection(ArrayList::new)); - Collections.reverse(tarTasksTmp); - tarTasks = new ArrayDeque<>(tarTasksTmp); - - listenerTasksPerRepo = tasksInOrder.stream() - .filter(task -> task.getPriority().equals(TaskPriority.LISTENER)) - .filter(task -> task.getSource().isLeft()) - .collect(groupingBy( - task -> task.getSource().getLeft().get().getRepoId(), - HashMap::new, - toCollection(ArrayDeque::new) - )); - - repos = listenerTasksPerRepo.keySet().stream() - .sorted() - .collect(Collectors.toCollection(ArrayDeque::new)); - - if (currentRepoId != null && !repos.isEmpty()) { - // Rotate the queue such that the current repo id is the first element. If that doesn't exist, - // use the repo id that would come after the current id. Usually, this would be the first id - // that's higher than the current id, but if the current id was already the highest id, this - // would be the smallest id. - // - // If all repo ids are smaller than the current id, this for loop will leave the queue in its - // initial state, which means that the smallest repo id is next (just like specified above). - for (int i = 0; i < repos.size(); i++) { - RepoId id = repos.peek(); - if (id.compareTo(currentRepoId) >= 0) { - break; - } - - // Rotate queue along by 1 - repos.add(repos.poll()); - } - } - } - - public Optional step() { - if (!manualTasks.isEmpty()) { - return Optional.of(manualTasks.poll()); - } - - if (!tarTasks.isEmpty()) { - return Optional.of(tarTasks.poll()); - } - - if (repos.isEmpty()) { - return Optional.empty(); - } - - RepoId repo = repos.poll(); - Queue repoQueue = listenerTasksPerRepo.get(repo); - - // This should always give back a task since we don't put repos with an empty task queue back - // into the repo queue and the grouping collector in the constructor doesn't create empty task - // queues either. - Task nextTask = repoQueue.remove(); - - if (!repoQueue.isEmpty()) { - // We only add the repo back into the repos queue if it still has at least one task. See - // comment above for more info. - repos.add(repo); - } - - return Optional.of(nextTask); - } - - public List stepAll() { - List tasksInOrder = new ArrayList<>(); - - while (true) { - Optional nextTask = step(); - if (nextTask.isPresent()) { - tasksInOrder.add(nextTask.get()); - } else { - break; - } - } - - return tasksInOrder; - } - - public Optional getCurrentRepoId() { - return Optional.ofNullable(repos.peek()); - } + private final Queue manualTasks; + private final Queue tarTasks; + + private final Map> listenerTasksPerRepo; + private final Queue repos; + + public Policy(List tasks, @Nullable RepoId currentRepoId) { + // Tasks ordered from newest to oldest (FILO) + List tasksInOrder = new ArrayList<>(tasks); + tasksInOrder.sort(comparing(Task::getUpdateTime)); + Collections.reverse(tasksInOrder); + + manualTasks = + tasksInOrder.stream() + .filter(task -> task.getPriority().equals(TaskPriority.MANUAL)) + .collect(toCollection(ArrayDeque::new)); + + // Tar tasks need to be reversed since they need a FIFO order, not FILO + ArrayList tarTasksTmp = + tasksInOrder.stream() + .filter(task -> task.getPriority().equals(TaskPriority.USER_CREATED)) + .collect(toCollection(ArrayList::new)); + Collections.reverse(tarTasksTmp); + tarTasks = new ArrayDeque<>(tarTasksTmp); + + listenerTasksPerRepo = + tasksInOrder.stream() + .filter(task -> task.getPriority().equals(TaskPriority.LISTENER)) + .filter(task -> task.getSource().isLeft()) + .collect( + groupingBy( + task -> task.getSource().getLeft().get().getRepoId(), + HashMap::new, + toCollection(ArrayDeque::new))); + + repos = + listenerTasksPerRepo.keySet().stream() + .sorted() + .collect(Collectors.toCollection(ArrayDeque::new)); + + if (currentRepoId != null && !repos.isEmpty()) { + // Rotate the queue such that the current repo id is the first element. If that doesn't exist, + // use the repo id that would come after the current id. Usually, this would be the first id + // that's higher than the current id, but if the current id was already the highest id, this + // would be the smallest id. + // + // If all repo ids are smaller than the current id, this for loop will leave the queue in its + // initial state, which means that the smallest repo id is next (just like specified above). + for (int i = 0; i < repos.size(); i++) { + RepoId id = repos.peek(); + if (id.compareTo(currentRepoId) >= 0) { + break; + } + + // Rotate queue along by 1 + repos.add(repos.poll()); + } + } + } + + public Optional step() { + if (!manualTasks.isEmpty()) { + return Optional.of(manualTasks.poll()); + } + + if (!tarTasks.isEmpty()) { + return Optional.of(tarTasks.poll()); + } + + if (repos.isEmpty()) { + return Optional.empty(); + } + + RepoId repo = repos.poll(); + Queue repoQueue = listenerTasksPerRepo.get(repo); + + // This should always give back a task since we don't put repos with an empty task queue back + // into the repo queue and the grouping collector in the constructor doesn't create empty task + // queues either. + Task nextTask = repoQueue.remove(); + + if (!repoQueue.isEmpty()) { + // We only add the repo back into the repos queue if it still has at least one task. See + // comment above for more info. + repos.add(repo); + } + + return Optional.of(nextTask); + } + + public List stepAll() { + List tasksInOrder = new ArrayList<>(); + + while (true) { + Optional nextTask = step(); + if (nextTask.isPresent()) { + tasksInOrder.add(nextTask.get()); + } else { + break; + } + } + + return tasksInOrder; + } + + public Optional getCurrentRepoId() { + return Optional.ofNullable(repos.peek()); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Queue.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Queue.java index fe28ec776..102e3367a 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Queue.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/queue/Queue.java @@ -32,197 +32,202 @@ */ public class Queue { - private final TaskWriteAccess taskAccess; - private final ArchiveReadAccess archiveAccess; - private final BenchmarkWriteAccess benchAccess; - - private final AtomicReference> currentRepoId; - - public Queue(TaskWriteAccess taskAccess, ArchiveReadAccess archiveAccess, - BenchmarkWriteAccess benchAccess) { - - this.taskAccess = taskAccess; - this.archiveAccess = archiveAccess; - this.benchAccess = benchAccess; - - currentRepoId = new AtomicReference<>(Optional.empty()); - } - - /** - * Gets the task with the specified id. - * - * @param taskId the id of the task - * @return the task - * @throws NoSuchTaskException if no task with the specified id exists - */ - public Task getTask(TaskId taskId) throws NoSuchTaskException { - return taskAccess.getTask(taskId); - } - - public void guardTaskExists(TaskId taskId) throws NoSuchTaskException { - taskAccess.getTask(taskId); - } - - /** - * @return all tasks currently in the queue. First come the tasks which are currently in progress, - * in no particular order. Then come the tasks which are currently not in progress, in the order - * they would be returned by successive calls to {@link #startNextTask()}. - */ - public List getAllTasksInOrder() { - List allTasks = taskAccess.getAllTasks(); - - List result = allTasks.stream() - .filter(Task::isInProgress) - .collect(Collectors.toCollection(ArrayList::new)); - - List tasksNotInProgress = allTasks.stream() - .filter(task -> !task.isInProgress()) - .collect(toList()); - - Policy policy = new Policy(tasksNotInProgress, currentRepoId.get().orElse(null)); - result.addAll(policy.stepAll()); - - return result; - } - - /** - * Find and start the task that is next in line. The returned task will be marked as "in progress" - * until it is completed via {@link #completeTask(NewRun)} or aborted via {@link - * #abortTask(TaskId)}. - * - * @return the task that was started - */ - public Optional startNextTask() { - return taskAccess.startTask(tasks -> { - Policy policy = new Policy(tasks, currentRepoId.get().orElse(null)); - Optional nextTask = policy.step(); - - if (nextTask.isPresent()) { - currentRepoId.set(policy.getCurrentRepoId()); - } - - return nextTask.map(Task::getId); - }); - } - - /** - * Convert the task with the same id as the {@link NewRun} into a full run. This removes the task - * from the queue and adds the newly created run into the database. - * - * @param result the result associated with the task - */ - public void completeTask(NewRun result) { - benchAccess.insertRun(result); - } - - /** - * Cancel the processing of the task with the given task id. This merely marks the task as "not in - * progress" and will not delete the task from the queue. - * - * @param taskId the id of the task - */ - public void abortTask(TaskId taskId) { - taskAccess.setTaskInProgress(taskId, false); - } - - /** - * Delete multiple from the queue by their id. - * - * @param taskIds the ids of the tasks - */ - public void deleteTasks(Collection taskIds) { - taskAccess.deleteTasks(taskIds); - } - - /** - * Delete all tasks from this queue. - */ - public void deleteAllTasks() { - taskAccess.deleteAllTasks(); - } - - /** - * A good way to check whether a task exists and is in progress. - * - * @param taskId the id of the task to check for - * @return true if the task exists and is in progress, false otherwise - */ - public boolean isTaskInProgress(TaskId taskId) { - return taskAccess.isTaskInProgress(taskId).orElse(false); - } - - /** - * Add a commit to the queue if no task for this commit already exists. - * - * @param author the new task's author - * @param repoId the id of the repo the commit belongs to - * @param hash the commit hash - * @param priority the new task's priority - * @return the new task or empty if there already was a task for this commit in the queue - */ - public Optional addCommit(String author, RepoId repoId, CommitHash hash, - TaskPriority priority) { - - return taskAccess.insertCommit(author, priority, repoId, hash); - } - - /** - * Add multiple commits to the queue in order. Does not add commits where a corresponding task - * already exists in the queue. - * - * @param author the new tasks' author - * @param repoId the id of the repo the commits belong to - * @param hashes the commit hashes - * @param priority the new tasks' priority - */ - public void addCommits(String author, RepoId repoId, List hashes, - TaskPriority priority) { - - taskAccess.insertCommits(author, priority, repoId, hashes); - } - - /** - * Add a tar file to the queue. - * - * @param author the new task's author - * @param priority the new task's priority - * @param repoId the id of the repo the tar file should to (or null) - * @param description the tar file's description - * @param inputStream the tar file's contents - * @return the newly created task - */ - public Task addTar(String author, TaskPriority priority, @Nullable RepoId repoId, - String description, InputStream inputStream) throws TaskCreationException { - - return taskAccess.insertTar(author, priority, description, repoId, inputStream); - } - - /** - * Changes the priority of the task with the specified task id. - * - * @param taskId the id of the task - * @param newPriority the new priority - */ - public void prioritizeTask(TaskId taskId, TaskPriority newPriority) { - taskAccess.setTaskPriority(taskId, newPriority); - } - - /** - * Transfers a task to the supplied {@link OutputStream}. - * - *

Note that the provided output stream will be closed after the transfer operation is done. - * - * @param taskId the id of the task to be transferred - * @param output the output to transfer the task to - * @throws NoSuchTaskException if no task with the given id exists - * @throws TarRetrieveException if the tar file could not be retrieved - * @throws TarTransferException if the tar file could not be transferred - */ - public void transferTask(TaskId taskId, OutputStream output) - throws NoSuchTaskException, TarRetrieveException, TarTransferException { - - Task task = taskAccess.getTask(taskId); - archiveAccess.transferTask(task, output); - } - + private final TaskWriteAccess taskAccess; + private final ArchiveReadAccess archiveAccess; + private final BenchmarkWriteAccess benchAccess; + + private final AtomicReference> currentRepoId; + + public Queue( + TaskWriteAccess taskAccess, + ArchiveReadAccess archiveAccess, + BenchmarkWriteAccess benchAccess) { + + this.taskAccess = taskAccess; + this.archiveAccess = archiveAccess; + this.benchAccess = benchAccess; + + currentRepoId = new AtomicReference<>(Optional.empty()); + } + + /** + * Gets the task with the specified id. + * + * @param taskId the id of the task + * @return the task + * @throws NoSuchTaskException if no task with the specified id exists + */ + public Task getTask(TaskId taskId) throws NoSuchTaskException { + return taskAccess.getTask(taskId); + } + + public void guardTaskExists(TaskId taskId) throws NoSuchTaskException { + taskAccess.getTask(taskId); + } + + /** + * @return all tasks currently in the queue. First come the tasks which are currently in progress, + * in no particular order. Then come the tasks which are currently not in progress, in the + * order they would be returned by successive calls to {@link #startNextTask()}. + */ + public List getAllTasksInOrder() { + List allTasks = taskAccess.getAllTasks(); + + List result = + allTasks.stream() + .filter(Task::isInProgress) + .collect(Collectors.toCollection(ArrayList::new)); + + List tasksNotInProgress = + allTasks.stream().filter(task -> !task.isInProgress()).collect(toList()); + + Policy policy = new Policy(tasksNotInProgress, currentRepoId.get().orElse(null)); + result.addAll(policy.stepAll()); + + return result; + } + + /** + * Find and start the task that is next in line. The returned task will be marked as "in progress" + * until it is completed via {@link #completeTask(NewRun)} or aborted via {@link + * #abortTask(TaskId)}. + * + * @return the task that was started + */ + public Optional startNextTask() { + return taskAccess.startTask( + tasks -> { + Policy policy = new Policy(tasks, currentRepoId.get().orElse(null)); + Optional nextTask = policy.step(); + + if (nextTask.isPresent()) { + currentRepoId.set(policy.getCurrentRepoId()); + } + + return nextTask.map(Task::getId); + }); + } + + /** + * Convert the task with the same id as the {@link NewRun} into a full run. This removes the task + * from the queue and adds the newly created run into the database. + * + * @param result the result associated with the task + */ + public void completeTask(NewRun result) { + benchAccess.insertRun(result); + } + + /** + * Cancel the processing of the task with the given task id. This merely marks the task as "not in + * progress" and will not delete the task from the queue. + * + * @param taskId the id of the task + */ + public void abortTask(TaskId taskId) { + taskAccess.setTaskInProgress(taskId, false); + } + + /** + * Delete multiple from the queue by their id. + * + * @param taskIds the ids of the tasks + */ + public void deleteTasks(Collection taskIds) { + taskAccess.deleteTasks(taskIds); + } + + /** Delete all tasks from this queue. */ + public void deleteAllTasks() { + taskAccess.deleteAllTasks(); + } + + /** + * A good way to check whether a task exists and is in progress. + * + * @param taskId the id of the task to check for + * @return true if the task exists and is in progress, false otherwise + */ + public boolean isTaskInProgress(TaskId taskId) { + return taskAccess.isTaskInProgress(taskId).orElse(false); + } + + /** + * Add a commit to the queue if no task for this commit already exists. + * + * @param author the new task's author + * @param repoId the id of the repo the commit belongs to + * @param hash the commit hash + * @param priority the new task's priority + * @return the new task or empty if there already was a task for this commit in the queue + */ + public Optional addCommit( + String author, RepoId repoId, CommitHash hash, TaskPriority priority) { + + return taskAccess.insertCommit(author, priority, repoId, hash); + } + + /** + * Add multiple commits to the queue in order. Does not add commits where a corresponding task + * already exists in the queue. + * + * @param author the new tasks' author + * @param repoId the id of the repo the commits belong to + * @param hashes the commit hashes + * @param priority the new tasks' priority + */ + public void addCommits( + String author, RepoId repoId, List hashes, TaskPriority priority) { + + taskAccess.insertCommits(author, priority, repoId, hashes); + } + + /** + * Add a tar file to the queue. + * + * @param author the new task's author + * @param priority the new task's priority + * @param repoId the id of the repo the tar file should to (or null) + * @param description the tar file's description + * @param inputStream the tar file's contents + * @return the newly created task + */ + public Task addTar( + String author, + TaskPriority priority, + @Nullable RepoId repoId, + String description, + InputStream inputStream) + throws TaskCreationException { + + return taskAccess.insertTar(author, priority, description, repoId, inputStream); + } + + /** + * Changes the priority of the task with the specified task id. + * + * @param taskId the id of the task + * @param newPriority the new priority + */ + public void prioritizeTask(TaskId taskId, TaskPriority newPriority) { + taskAccess.setTaskPriority(taskId, newPriority); + } + + /** + * Transfers a task to the supplied {@link OutputStream}. + * + *

Note that the provided output stream will be closed after the transfer operation is done. + * + * @param taskId the id of the task to be transferred + * @param output the output to transfer the task to + * @throws NoSuchTaskException if no task with the given id exists + * @throws TarRetrieveException if the tar file could not be retrieved + * @throws TarTransferException if the tar file could not be transferred + */ + public void transferTask(TaskId taskId, OutputStream output) + throws NoSuchTaskException, TarRetrieveException, TarTransferException { + + Task task = taskAccess.getTask(taskId); + archiveAccess.transferTask(task, output); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRun.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRun.java index ffa868597..3d0c3c3ab 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRun.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRun.java @@ -10,27 +10,24 @@ */ public class SignificantRun { - private final Run run; - private final SignificanceReasons reasons; + private final Run run; + private final SignificanceReasons reasons; - public SignificantRun(Run run, SignificanceReasons reasons) { - this.run = run; - this.reasons = reasons; - } + public SignificantRun(Run run, SignificanceReasons reasons) { + this.run = run; + this.reasons = reasons; + } - public Run getRun() { - return run; - } + public Run getRun() { + return run; + } - public SignificanceReasons getReasons() { - return reasons; - } + public SignificanceReasons getReasons() { + return reasons; + } - @Override - public String toString() { - return "SignificantRun{" + - "run=" + run + - ", reasons=" + reasons + - '}'; - } + @Override + public String toString() { + return "SignificantRun{" + "run=" + run + ", reasons=" + reasons + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRunsCollector.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRunsCollector.java index 7a63fbd2e..168908e7e 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRunsCollector.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/recentruns/SignificantRunsCollector.java @@ -30,159 +30,166 @@ import java.util.Optional; import java.util.Set; -/** - * This class detects significant runs. - */ +/** This class detects significant runs. */ public class SignificantRunsCollector { - private static final int BATCH_SIZE = 50; - private static final int MAX_TRIES = 10; - - private final SignificanceFactors significanceFactors; - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final RunCache runCache; - private final LatestRunCache latestRunCache; - private final SignificanceDetector significanceDetector; - - public SignificantRunsCollector(SignificanceFactors significanceFactors, - BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess, RunCache runCache, LatestRunCache latestRunCache, - SignificanceDetector significanceDetector) { - - this.significanceFactors = significanceFactors; - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.runCache = runCache; - this.latestRunCache = latestRunCache; - this.significanceDetector = significanceDetector; - } - - /** - * Tries to fetch the {@link SignificantRun} for a given {@link Run}, if the run has any - * significant changes. - * - * @param run the run - * @return the significant run if it was significant, empty otherwise - */ - @Timed(histogram = true) - public Optional getSignificantRun(Run run) { - Optional commitSource = run.getSource().getLeft(); - if (commitSource.isEmpty()) { - return Optional.empty(); - } - CommitSource source = commitSource.get(); - - Set parentHashes = commitAccess.getParentHashes( - source.getRepoId(), - source.getHash() - ); - Collection parentRuns = latestRunCache - .getLatestRuns(benchmarkAccess, runCache, source.getRepoId(), parentHashes) - .values(); - - Set significantDimensions = dimensionAccess.getSignificantDimensions(); - - return getSignificantRun(run, Map.of(source, parentRuns), significantDimensions); - } - - /** - * Find the specified amount of significant runs, starting at the most recent run and going - * backwards from there. - * - * @param amount the maximum amount of significant runs to return - * @return the significant runs which were found - */ - @Timed(histogram = true) - public List collectMostRecent(int amount) { - List runs = new ArrayList<>(); - - for (int i = 0; i < MAX_TRIES && runs.size() < amount; i++) { - runs.addAll(collectBatch(i * BATCH_SIZE)); - } - - return runs.subList(0, Math.min(amount, runs.size())); - } - - private List collectBatch(int skip) { - // 1. Load runs - // 2. Load parent runs per run - // 2.1. Load commits per RepoId - // 2.2. Load parent runs per CommitSource - // 3. Filter out significant runs - - List recentRunIds = benchmarkAccess.getRecentRunIds(skip, BATCH_SIZE); - List runs = runCache.getRunsInOrder(benchmarkAccess, recentRunIds); - Map> commitsPerRepo = getCommitsPerRepo(runs); - Map> parentRunsPerSource = getParentRunsPerSource(commitsPerRepo); - Set significantDimensions = dimensionAccess.getSignificantDimensions(); - - return runs.stream() - .flatMap(run -> getSignificantRun(run, parentRunsPerSource, significantDimensions).stream()) - .collect(toList()); - } - - private Map> getCommitsPerRepo(Collection runs) { - Map> hashesByRepo = runs.stream() - .flatMap(run -> run.getSource().getLeft().stream()) - .collect(groupingBy(CommitSource::getRepoId, mapping(CommitSource::getHash, toSet()))); - - List commits = hashesByRepo.entrySet().stream() - .flatMap(entry -> commitAccess.getCommits(entry.getKey(), entry.getValue()).stream()) - .collect(toList()); - - return commitAccess.promoteCommits(commits).stream() - .collect(groupingBy(Commit::getRepoId, toList())); - } - - private Map> getParentRunsPerSource( - Map> commitsPerRepo) { - - Map> parentRuns = new HashMap<>(); - commitsPerRepo.forEach((repoId, commits) -> { - Set parentHashes = commits.stream() - .flatMap(commit -> commit.getParentHashes().stream()) - .collect(toSet()); - Map latestRuns = latestRunCache - .getLatestRuns(benchmarkAccess, runCache, repoId, parentHashes); - parentRuns.put(repoId, latestRuns); - }); - - return commitsPerRepo.values().stream() - .flatMap(Collection::stream) - .collect(toMap( - CommitSource::fromCommit, - commit -> { - // There will always be an entry for the repo id as long as commitsPerRepo doesn't contain - // any commits under the wrong repo id - Map runByHash = parentRuns.get(commit.getRepoId()); - - return commit.getParentHashes().stream() - // Ignoring parent commits without associated run, which do occur occasionally - .flatMap(hash -> Optional.ofNullable(runByHash.get(hash)).stream()) - .collect(toList()); - } - )); - } - - /** - * Check if a run is significant. - * - * @param run a run - * @param parentMap a map of all known commits' parent runs. Is not required to contain an entry - * for this particular run - * @param significantDimensions all significant dimensions - * @return a {@link SignificantRun} if the run is significant, {@link Optional#empty()} otherwise - */ - private Optional getSignificantRun(Run run, - Map> parentMap, Set significantDimensions) { - - return run.getSource() - .getLeft() - .map(source -> Optional.ofNullable(parentMap.get(source)).orElse(List.of())) - .flatMap(parents -> significanceDetector.getSignificance(run, parents, significantDimensions)) - .map(reasons -> new SignificantRun(run, reasons)); - } + private static final int BATCH_SIZE = 50; + private static final int MAX_TRIES = 10; + + private final SignificanceFactors significanceFactors; + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final RunCache runCache; + private final LatestRunCache latestRunCache; + private final SignificanceDetector significanceDetector; + + public SignificantRunsCollector( + SignificanceFactors significanceFactors, + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + RunCache runCache, + LatestRunCache latestRunCache, + SignificanceDetector significanceDetector) { + + this.significanceFactors = significanceFactors; + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.runCache = runCache; + this.latestRunCache = latestRunCache; + this.significanceDetector = significanceDetector; + } + + /** + * Tries to fetch the {@link SignificantRun} for a given {@link Run}, if the run has any + * significant changes. + * + * @param run the run + * @return the significant run if it was significant, empty otherwise + */ + @Timed(histogram = true) + public Optional getSignificantRun(Run run) { + Optional commitSource = run.getSource().getLeft(); + if (commitSource.isEmpty()) { + return Optional.empty(); + } + CommitSource source = commitSource.get(); + + Set parentHashes = + commitAccess.getParentHashes(source.getRepoId(), source.getHash()); + Collection parentRuns = + latestRunCache + .getLatestRuns(benchmarkAccess, runCache, source.getRepoId(), parentHashes) + .values(); + + Set significantDimensions = dimensionAccess.getSignificantDimensions(); + + return getSignificantRun(run, Map.of(source, parentRuns), significantDimensions); + } + + /** + * Find the specified amount of significant runs, starting at the most recent run and going + * backwards from there. + * + * @param amount the maximum amount of significant runs to return + * @return the significant runs which were found + */ + @Timed(histogram = true) + public List collectMostRecent(int amount) { + List runs = new ArrayList<>(); + + for (int i = 0; i < MAX_TRIES && runs.size() < amount; i++) { + runs.addAll(collectBatch(i * BATCH_SIZE)); + } + + return runs.subList(0, Math.min(amount, runs.size())); + } + + private List collectBatch(int skip) { + // 1. Load runs + // 2. Load parent runs per run + // 2.1. Load commits per RepoId + // 2.2. Load parent runs per CommitSource + // 3. Filter out significant runs + + List recentRunIds = benchmarkAccess.getRecentRunIds(skip, BATCH_SIZE); + List runs = runCache.getRunsInOrder(benchmarkAccess, recentRunIds); + Map> commitsPerRepo = getCommitsPerRepo(runs); + Map> parentRunsPerSource = getParentRunsPerSource(commitsPerRepo); + Set significantDimensions = dimensionAccess.getSignificantDimensions(); + + return runs.stream() + .flatMap(run -> getSignificantRun(run, parentRunsPerSource, significantDimensions).stream()) + .collect(toList()); + } + + private Map> getCommitsPerRepo(Collection runs) { + Map> hashesByRepo = + runs.stream() + .flatMap(run -> run.getSource().getLeft().stream()) + .collect(groupingBy(CommitSource::getRepoId, mapping(CommitSource::getHash, toSet()))); + + List commits = + hashesByRepo.entrySet().stream() + .flatMap(entry -> commitAccess.getCommits(entry.getKey(), entry.getValue()).stream()) + .collect(toList()); + + return commitAccess.promoteCommits(commits).stream() + .collect(groupingBy(Commit::getRepoId, toList())); + } + + private Map> getParentRunsPerSource( + Map> commitsPerRepo) { + + Map> parentRuns = new HashMap<>(); + commitsPerRepo.forEach( + (repoId, commits) -> { + Set parentHashes = + commits.stream() + .flatMap(commit -> commit.getParentHashes().stream()) + .collect(toSet()); + Map latestRuns = + latestRunCache.getLatestRuns(benchmarkAccess, runCache, repoId, parentHashes); + parentRuns.put(repoId, latestRuns); + }); + + return commitsPerRepo.values().stream() + .flatMap(Collection::stream) + .collect( + toMap( + CommitSource::fromCommit, + commit -> { + // There will always be an entry for the repo id as long as commitsPerRepo doesn't + // contain + // any commits under the wrong repo id + Map runByHash = parentRuns.get(commit.getRepoId()); + + return commit.getParentHashes().stream() + // Ignoring parent commits without associated run, which do occur occasionally + .flatMap(hash -> Optional.ofNullable(runByHash.get(hash)).stream()) + .collect(toList()); + })); + } + + /** + * Check if a run is significant. + * + * @param run a run + * @param parentMap a map of all known commits' parent runs. Is not required to contain an entry + * for this particular run + * @param significantDimensions all significant dimensions + * @return a {@link SignificantRun} if the run is significant, {@link Optional#empty()} otherwise + */ + private Optional getSignificantRun( + Run run, Map> parentMap, Set significantDimensions) { + + return run.getSource() + .getLeft() + .map(source -> Optional.ofNullable(parentMap.get(source)).orElse(List.of())) + .flatMap( + parents -> significanceDetector.getSignificance(run, parents, significantDimensions)) + .map(reasons -> new SignificantRun(run, reasons)); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/DimensionDifference.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/DimensionDifference.java index de1595909..b817db158 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/DimensionDifference.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/DimensionDifference.java @@ -5,72 +5,73 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * The difference between two runs in a single dimension. - */ +/** The difference between two runs in a single dimension. */ public class DimensionDifference { - private final Dimension dimension; - private final double first; - private final double second; - private final RunId oldRunId; - @Nullable - private final Double secondStddev; + private final Dimension dimension; + private final double first; + private final double second; + private final RunId oldRunId; + @Nullable private final Double secondStddev; - public DimensionDifference(Dimension dimension, double first, double second, RunId oldRunId, - @Nullable Double secondStddev) { + public DimensionDifference( + Dimension dimension, + double first, + double second, + RunId oldRunId, + @Nullable Double secondStddev) { - this.dimension = dimension; - this.first = first; - this.second = second; - this.oldRunId = oldRunId; - this.secondStddev = secondStddev; - } + this.dimension = dimension; + this.first = first; + this.second = second; + this.oldRunId = oldRunId; + this.secondStddev = secondStddev; + } - public Dimension getDimension() { - return dimension; - } + public Dimension getDimension() { + return dimension; + } - public double getFirst() { - return first; - } + public double getFirst() { + return first; + } - public double getSecond() { - return second; - } + public double getSecond() { + return second; + } - public RunId getOldRunId() { - return oldRunId; - } + public RunId getOldRunId() { + return oldRunId; + } - public Optional getSecondStddev() { - return Optional.ofNullable(secondStddev); - } + public Optional getSecondStddev() { + return Optional.ofNullable(secondStddev); + } - /** - * @return second - first - */ - public double getDiff() { - return second - first; - } + /** + * @return second - first + */ + public double getDiff() { + return second - first; + } - /** - * @return (second - first) / first, if first != 0 - */ - public Optional getReldiff() { - if (first == 0) { // Don't divide by 0 - return Optional.empty(); - } + /** + * @return (second - first) / first, if first != 0 + */ + public Optional getReldiff() { + if (first == 0) { // Don't divide by 0 + return Optional.empty(); + } - return Optional.of((second - first) / first); - } + return Optional.of((second - first) / first); + } - /** - * @return (second - first) / second stddev, if second stddev exists - */ - public Optional getStddevDiff() { - return getSecondStddev() - .filter(stddev -> stddev != 0) // Don't divide by 0 - .map(stddev -> getDiff() / stddev); - } + /** + * @return (second - first) / second stddev, if second stddev exists + */ + public Optional getStddevDiff() { + return getSecondStddev() + .filter(stddev -> stddev != 0) // Don't divide by 0 + .map(stddev -> getDiff() / stddev); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparator.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparator.java index c9b006fbb..165f8803f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparator.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparator.java @@ -17,58 +17,57 @@ */ public class RunComparator { - private final SignificanceFactors significanceFactors; + private final SignificanceFactors significanceFactors; - public RunComparator(SignificanceFactors significanceFactors) { - this.significanceFactors = significanceFactors; - } + public RunComparator(SignificanceFactors significanceFactors) { + this.significanceFactors = significanceFactors; + } - /** - * Compare two {@link Run}s against each other. - * - * @param first the first run - * @param second the second run - * @return the differences between the runs - */ - public RunComparison compare(Run first, Run second) { - Map firstMap = getMeasurementsMap(first); - Map secondMap = getMeasurementsMap(second); + /** + * Compare two {@link Run}s against each other. + * + * @param first the first run + * @param second the second run + * @return the differences between the runs + */ + public RunComparison compare(Run first, Run second) { + Map firstMap = getMeasurementsMap(first); + Map secondMap = getMeasurementsMap(second); - HashSet commonNames = new HashSet<>(firstMap.keySet()); - commonNames.retainAll(secondMap.keySet()); + HashSet commonNames = new HashSet<>(firstMap.keySet()); + commonNames.retainAll(secondMap.keySet()); - ArrayList differences = new ArrayList<>(); - for (Dimension dimension : commonNames) { - MeasurementValues firstValues = firstMap.get(dimension); - MeasurementValues secondValues = secondMap.get(dimension); + ArrayList differences = new ArrayList<>(); + for (Dimension dimension : commonNames) { + MeasurementValues firstValues = firstMap.get(dimension); + MeasurementValues secondValues = secondMap.get(dimension); - DimensionDifference difference = new DimensionDifference( - dimension, - firstValues.getAverageValue(), - secondValues.getAverageValue(), - first.getId(), - secondValues.getStddevWith(significanceFactors).orElse(null) - ); - differences.add(difference); - } + DimensionDifference difference = + new DimensionDifference( + dimension, + firstValues.getAverageValue(), + secondValues.getAverageValue(), + first.getId(), + secondValues.getStddevWith(significanceFactors).orElse(null)); + differences.add(difference); + } - return new RunComparison(first, second, differences); - } + return new RunComparison(first, second, differences); + } - private Map getMeasurementsMap(Run run) { - Optional> right = run.getResult().getRight(); - if (right.isEmpty()) { - return Map.of(); - } + private Map getMeasurementsMap(Run run) { + Optional> right = run.getResult().getRight(); + if (right.isEmpty()) { + return Map.of(); + } - HashMap measurements = new HashMap<>(); - for (Measurement measurement : right.get()) { - Optional content = measurement.getContent().getRight(); - content.ifPresent( - measurementValues -> measurements.put(measurement.getDimension(), measurementValues) - ); - } + HashMap measurements = new HashMap<>(); + for (Measurement measurement : right.get()) { + Optional content = measurement.getContent().getRight(); + content.ifPresent( + measurementValues -> measurements.put(measurement.getDimension(), measurementValues)); + } - return measurements; - } + return measurements; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparison.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparison.java index a28b8e223..8ac9120a9 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparison.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/runcomparison/RunComparison.java @@ -6,36 +6,32 @@ import java.util.Set; import java.util.stream.Collectors; -/** - * The result of comparing two runs. - */ +/** The result of comparing two runs. */ public class RunComparison { - private final Run first; - private final Run second; - private final List differences; - - public RunComparison(Run first, Run second, List differences) { - this.first = first; - this.second = second; - this.differences = differences; - } - - public Run getFirst() { - return first; - } - - public Run getSecond() { - return second; - } - - public List getDifferences() { - return differences; - } - - public Set getDimensions() { - return differences.stream() - .map(DimensionDifference::getDimension) - .collect(Collectors.toSet()); - } + private final Run first; + private final Run second; + private final List differences; + + public RunComparison(Run first, Run second, List differences) { + this.first = first; + this.second = second; + this.differences = differences; + } + + public Run getFirst() { + return first; + } + + public Run getSecond() { + return second; + } + + public List getDifferences() { + return differences; + } + + public Set getDimensions() { + return differences.stream().map(DimensionDifference::getDimension).collect(Collectors.toSet()); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceDetector.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceDetector.java index 8f37d6ad6..6a40470ed 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceDetector.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceDetector.java @@ -15,75 +15,81 @@ public class SignificanceDetector { - private final SignificanceFactors significanceFactors; - private final RunComparator runComparator; + private final SignificanceFactors significanceFactors; + private final RunComparator runComparator; - public SignificanceDetector(SignificanceFactors significanceFactors, - RunComparator runComparator) { + public SignificanceDetector( + SignificanceFactors significanceFactors, RunComparator runComparator) { - this.significanceFactors = significanceFactors; - this.runComparator = runComparator; - } + this.significanceFactors = significanceFactors; + this.runComparator = runComparator; + } - /** - * Check whether a run is significant when compared to multiple other runs. - * - * @param run the run whose significance to determine - * @param comparedTo the other runs to compare against - * @param significantDimensions the currently significant dimensions - * @return a {@link SignificanceReasons} instance detailing why the run is significant, or {@code - * Optional.empty()} if the run is not significant. - */ - public Optional getSignificance(Run run, Collection comparedTo, - Set significantDimensions) { + /** + * Check whether a run is significant when compared to multiple other runs. + * + * @param run the run whose significance to determine + * @param comparedTo the other runs to compare against + * @param significantDimensions the currently significant dimensions + * @return a {@link SignificanceReasons} instance detailing why the run is significant, or {@code + * Optional.empty()} if the run is not significant. + */ + public Optional getSignificance( + Run run, Collection comparedTo, Set significantDimensions) { - if (run.getResult().getRight().isEmpty()) { - return Optional.of(new SignificanceReasons(List.of(), List.of(), true)); - } + if (run.getResult().getRight().isEmpty()) { + return Optional.of(new SignificanceReasons(List.of(), List.of(), true)); + } - List significantFailedDimensions = run.getResult() - .getRight() - .get() - .stream() - .filter(measurement -> measurement.getContent().isLeft()) - .map(Measurement::getDimension) - .filter(significantDimensions::contains) - .sorted() - .collect(toList()); + List significantFailedDimensions = + run.getResult().getRight().get().stream() + .filter(measurement -> measurement.getContent().isLeft()) + .map(Measurement::getDimension) + .filter(significantDimensions::contains) + .sorted() + .collect(toList()); - List significantDifferences = comparedTo.stream() - .map(first -> runComparator.compare(first, run)) - .flatMap(comparison -> comparison.getDifferences().stream()) - .filter(difference -> significantDimensions.contains(difference.getDimension())) - .filter(this::isSignificantDifference) - .sorted(Comparator.comparing(DimensionDifference::getDimension)) - .collect(toList()); + List significantDifferences = + comparedTo.stream() + .map(first -> runComparator.compare(first, run)) + .flatMap(comparison -> comparison.getDifferences().stream()) + .filter(difference -> significantDimensions.contains(difference.getDimension())) + .filter(this::isSignificantDifference) + .sorted(Comparator.comparing(DimensionDifference::getDimension)) + .collect(toList()); - if (!significantDifferences.isEmpty() || !significantFailedDimensions.isEmpty()) { - return Optional - .of(new SignificanceReasons(significantDifferences, significantFailedDimensions, false)); - } else { - return Optional.empty(); - } - } + if (!significantDifferences.isEmpty() || !significantFailedDimensions.isEmpty()) { + return Optional.of( + new SignificanceReasons(significantDifferences, significantFailedDimensions, false)); + } else { + return Optional.empty(); + } + } - private boolean isSignificantDifference(DimensionDifference diff) { - if (diff.getDimension().getBenchmark().startsWith("~")) - // 1B instructions - return Math.abs(diff.getDiff()) >= 10_000_000_000L; + private boolean isSignificantDifference(DimensionDifference diff) { + if (diff.getDimension().getBenchmark().startsWith("~")) + // 1B instructions + return Math.abs(diff.getDiff()) >= 10_000_000_000L; - boolean relSignificant = diff.getReldiff() - .map(reldiff -> Math.abs(reldiff) >= significanceFactors.getRelativeThreshold()) - // There is no reldiff if the first value is 0. But if the second value is also zero, that - // hardly constitutes a significant difference. Otherwise, it is a move away from 0, which is - // always significant. - .orElse(diff.getFirst() != diff.getSecond()); + boolean relSignificant = + diff.getReldiff() + .map(reldiff -> Math.abs(reldiff) >= significanceFactors.getRelativeThreshold()) + // There is no reldiff if the first value is 0. But if the second value is also zero, + // that + // hardly constitutes a significant difference. Otherwise, it is a move away from 0, + // which is + // always significant. + .orElse(diff.getFirst() != diff.getSecond()); - boolean stddevSignificant = diff.getSecondStddev() - .map(stddev -> Math.abs(diff.getDiff()) >= significanceFactors.getStddevThreshold() * stddev) - // If there is no stddev, this check should not prevent differences from being significant - .orElse(true); + boolean stddevSignificant = + diff.getSecondStddev() + .map( + stddev -> + Math.abs(diff.getDiff()) >= significanceFactors.getStddevThreshold() * stddev) + // If there is no stddev, this check should not prevent differences from being + // significant + .orElse(true); - return relSignificant && stddevSignificant; - } + return relSignificant && stddevSignificant; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceFactors.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceFactors.java index c637d3cfd..a5d7469d1 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceFactors.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceFactors.java @@ -6,37 +6,37 @@ */ public class SignificanceFactors { - private final double relativeThreshold; - private final double stddevThreshold; - private final int minStddevAmount; - - public SignificanceFactors(double relativeThreshold, double stddevThreshold, - int minStddevAmount) { - - if (relativeThreshold < 0) { - throw new IllegalArgumentException("relative threshold must be >= 0"); - } - if (stddevThreshold < 0) { - throw new IllegalArgumentException("stddev threshold must be >= 0"); - } - if (minStddevAmount < 2) { - throw new IllegalArgumentException("minimum stddev amount must be at least 2"); - } - - this.relativeThreshold = relativeThreshold; - this.stddevThreshold = stddevThreshold; - this.minStddevAmount = minStddevAmount; - } - - public double getRelativeThreshold() { - return relativeThreshold; - } - - public double getStddevThreshold() { - return stddevThreshold; - } - - public int getMinStddevAmount() { - return minStddevAmount; - } + private final double relativeThreshold; + private final double stddevThreshold; + private final int minStddevAmount; + + public SignificanceFactors( + double relativeThreshold, double stddevThreshold, int minStddevAmount) { + + if (relativeThreshold < 0) { + throw new IllegalArgumentException("relative threshold must be >= 0"); + } + if (stddevThreshold < 0) { + throw new IllegalArgumentException("stddev threshold must be >= 0"); + } + if (minStddevAmount < 2) { + throw new IllegalArgumentException("minimum stddev amount must be at least 2"); + } + + this.relativeThreshold = relativeThreshold; + this.stddevThreshold = stddevThreshold; + this.minStddevAmount = minStddevAmount; + } + + public double getRelativeThreshold() { + return relativeThreshold; + } + + public double getStddevThreshold() { + return stddevThreshold; + } + + public int getMinStddevAmount() { + return minStddevAmount; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceReasons.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceReasons.java index 799b99903..bece59530 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceReasons.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/data/significance/SignificanceReasons.java @@ -10,43 +10,48 @@ public class SignificanceReasons { - private final List significantDifferences; - private final List significantFailedDimensions; - private final boolean entireRunFailed; - - public SignificanceReasons(List significantDifferences, - List significantFailedDimensions, boolean entireRunFailed) { - - this.significantDifferences = significantDifferences; - this.significantFailedDimensions = significantFailedDimensions; - this.entireRunFailed = entireRunFailed; - } - - public List getSignificantDifferences() { - return significantDifferences; - } - - public List getSignificantFailedDimensions() { - return significantFailedDimensions; - } - - public boolean isEntireRunFailed() { - return entireRunFailed; - } - - public Set getDimensions() { - return Stream.concat( - significantDifferences.stream().map(DimensionDifference::getDimension), - significantFailedDimensions.stream() - ).collect(toSet()); - } - - @Override - public String toString() { - return "SignificanceReasons{" + - "significantDifferences=" + significantDifferences + - ", significantFailedDimensions=" + significantFailedDimensions + - ", entireRunFailed=" + entireRunFailed + - '}'; - } + private final List significantDifferences; + private final List significantFailedDimensions; + private final boolean entireRunFailed; + + public SignificanceReasons( + List significantDifferences, + List significantFailedDimensions, + boolean entireRunFailed) { + + this.significantDifferences = significantDifferences; + this.significantFailedDimensions = significantFailedDimensions; + this.entireRunFailed = entireRunFailed; + } + + public List getSignificantDifferences() { + return significantDifferences; + } + + public List getSignificantFailedDimensions() { + return significantFailedDimensions; + } + + public boolean isEntireRunFailed() { + return entireRunFailed; + } + + public Set getDimensions() { + return Stream.concat( + significantDifferences.stream().map(DimensionDifference::getDimension), + significantFailedDimensions.stream()) + .collect(toSet()); + } + + @Override + public String toString() { + return "SignificanceReasons{" + + "significantDifferences=" + + significantDifferences + + ", significantFailedDimensions=" + + significantFailedDimensions + + ", entireRunFailed=" + + entireRunFailed + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/InvalidRemoteUrlException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/InvalidRemoteUrlException.java index 8804f1ad1..a4fc9f34f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/InvalidRemoteUrlException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/InvalidRemoteUrlException.java @@ -6,22 +6,25 @@ */ public class InvalidRemoteUrlException extends RuntimeException { - private final String realRemoteUrl; - private final String targetRemoteUrl; + private final String realRemoteUrl; + private final String targetRemoteUrl; - public InvalidRemoteUrlException(String realRemoteUrl, String targetRemoteUrl) { - super("Real remote url " + realRemoteUrl - + " does not match target remote url " + targetRemoteUrl); + public InvalidRemoteUrlException(String realRemoteUrl, String targetRemoteUrl) { + super( + "Real remote url " + + realRemoteUrl + + " does not match target remote url " + + targetRemoteUrl); - this.realRemoteUrl = realRemoteUrl; - this.targetRemoteUrl = targetRemoteUrl; - } + this.realRemoteUrl = realRemoteUrl; + this.targetRemoteUrl = targetRemoteUrl; + } - public String getRealRemoteUrl() { - return realRemoteUrl; - } + public String getRealRemoteUrl() { + return realRemoteUrl; + } - public String getTargetRemoteUrl() { - return targetRemoteUrl; - } + public String getTargetRemoteUrl() { + return targetRemoteUrl; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/Listener.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/Listener.java index f9e13826f..63dcd558b 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/Listener.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/Listener.java @@ -47,327 +47,334 @@ */ public class Listener { - private static final Logger LOGGER = LoggerFactory.getLogger(Listener.class); - private static final String QUEUE_AUTHOR = "Listener"; - private static final TaskPriority TASK_PRIORITY = TaskPriority.LISTENER; - - private final DatabaseStorage databaseStorage; - private final RepoStorage repoStorage; - - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final RepoWriteAccess repoAccess; - private final BenchRepo benchRepo; - private final SignificanceDetector significanceDetector; - private final Queue queue; - - private final Duration vacuumInterval; - private Instant lastVacuum; - - private final String frontendUrl; - - // An explicit reference to the executor is kept to ensure it will never accidentally be garbage - // collected. This might not be strictly necessary, but it doesn't hurt either. - @SuppressWarnings("FieldCanBeLocal") - private final ScheduledExecutorService executor; - - /** - * Constructs a new listener instance. - * - * @param databaseStorage used to read and write various kinds of data - * @param repoStorage used to manipulate locally cloned repos - * @param repoAccess used to read repo data - * @param benchRepo used to keep the bench repo up-to-date - * @param queue used to add new commits to the queue - * @param pollInterval the time the listener waits between updating its repos - */ - public Listener( - DatabaseStorage databaseStorage, RepoStorage repoStorage, BenchmarkReadAccess benchmarkAccess, - CommitReadAccess commitAccess, DimensionReadAccess dimensionAccess, RepoWriteAccess repoAccess, - BenchRepo benchRepo, SignificanceDetector significanceDetector, Queue queue, - Duration pollInterval, Duration vacuumInterval, String frontendUrl) { - - this.databaseStorage = databaseStorage; - this.repoStorage = repoStorage; - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.repoAccess = repoAccess; - this.benchRepo = benchRepo; - this.significanceDetector = significanceDetector; - this.queue = queue; - - this.vacuumInterval = vacuumInterval; - this.lastVacuum = Instant.now(); - - this.frontendUrl = frontendUrl; - - executor = Executors.newSingleThreadScheduledExecutor(); - executor.scheduleWithFixedDelay( - this::onUpdate, - 0, - pollInterval.toSeconds(), - TimeUnit.SECONDS - ); - } - - /** - * This function is called regularly by the executor. - */ - private void onUpdate() { - updateAllRepos(); - runAnalyze(); - vacuumIfNecessary(); - } - - /** - * Re-fetch and update the bench repo and all repos that are in the db. Remove all local clones of - * repos that are no longer in the db and (re-)clone all repos that are not cloned or whose clones - * are broken. - */ - @Timed(histogram = true) - public synchronized void updateAllRepos() { - LOGGER.info("Updating all repos"); - - tryToSynchronizeCommitsForBenchRepo(); - - List currentRepos = repoAccess.getAllRepos(); - tryToDeleteLocalReposExcept(currentRepos); - - for (Repo repo : currentRepos) { - tryToUpdateRepo(repo); - } - } - - private void tryToSynchronizeCommitsForBenchRepo() { - try { - LOGGER.debug("Synchronizing bench repo"); - synchronizeCommitsForBenchRepo(); - } catch (Exception e) { - LOGGER.warn("Failed to synchronize bench repo", e); - } - } - - private void synchronizeCommitsForBenchRepo() throws SynchronizeCommitsException { - genericSynchronizeCommits( - "benchrepo", - benchRepo.getDirName(), - benchRepo.getRemoteUrl(), - jgitRepo -> {/* The bench repo commits are not stored in the db. */} - ); - } - - private void tryToDeleteLocalReposExcept(List reposToKeep) { - try { - LOGGER.debug("Cleaning up old repos"); - deleteLocalReposExcept(reposToKeep); - } catch (IOException e) { - LOGGER.warn("Failed to clean up old repos", e); - } - } - - private void deleteLocalReposExcept(List reposToKeep) throws IOException { - Set reposThatShouldExist = Stream.concat( - Stream.of(benchRepo.getDirName()), - reposToKeep.stream() - .map(Repo::getId) - .map(RepoId::getDirectoryName) - ).collect(toSet()); - - Set reposThatShouldNotExist = repoStorage.getRepoDirectories().stream() - .map(Path::getFileName) - .map(Path::toString) - .filter(name -> !reposThatShouldExist.contains(name)) - .collect(toSet()); - - for (String repoName : reposThatShouldNotExist) { - LOGGER.info("Deleting old repo {}", repoName); - repoStorage.deleteRepository(repoName); - } - } - - private void tryToUpdateRepo(Repo repo) { - try { - LOGGER.debug("Updating repo {}", repo.getId()); - updateRepo(repo); - } catch (GithubApiError e) { - LOGGER.warn("Failed to update repo {}", repo.getId()); - LOGGER.warn("{}", e.getMessage()); - } catch (Exception e) { - LOGGER.warn("Failed to update repo {}", repo.getId(), e); - } - } - - private void updateRepo(Repo repo) - throws SynchronizeCommitsException, IOException, InterruptedException, URISyntaxException, GithubApiError { - - Optional ghIntOpt = GithubPrInteractor.fromRepo( - repo, - databaseStorage, - benchmarkAccess, - commitAccess, - dimensionAccess, - significanceDetector, - queue, - frontendUrl - ); - - if (ghIntOpt.isPresent()) { - GithubPrInteractor ghInteractor = ghIntOpt.get(); - - ghInteractor.searchForNewPrCommands(); - - synchronizeCommitsForRepo(repo); - - ghInteractor.markNewPrCommandsAsSeen(); - ghInteractor.addNewPrCommandsToQueue(); - ghInteractor.replyToFinishedPrCommands(); - ghInteractor.replyToErroredPrCommands(); - } else { - synchronizeCommitsForRepo(repo); - } - } - - /** - * Fetch latest commits for repo, store them in the database and add them to the queue if - * necessary. - * - * @throws SynchronizeCommitsException if any of these steps didn't work. This might be because - * the remote url is invalid or inaccessible. - */ - @Timed(histogram = true) - public void synchronizeCommitsForRepo(Repo repo) throws SynchronizeCommitsException { - genericSynchronizeCommits( - repo.getName() + " (" + repo.getIdAsString() + ")", - repo.getId().getDirectoryName(), - repo.getRemoteUrlAsString(), - jgitRepo -> updateCommitsInDb(repo, jgitRepo) - ); - } - - /** - * Abstracts away updating a repo so the same updating logic can be applied to the bench repo. - * - * @param repoName name of the repo, used only for logging - * @param repoDirName name of the repo's directory. This is not just a {@link RepoId} because - * this function should also work for the bench repo. - * @param targetRemoteUrl the remote url the repo should have. The repo is recloned if its actual - * remote url doesn't match this target remote url. - * @param jgitRepoAction this function will be called on the jgit {@link Repository} if cloning - * or recloning was successful - * @throws SynchronizeCommitsException if the commits in the db could not be updated - * successfully. This may be because the remote url is invalid or inaccessible. - */ - private synchronized void genericSynchronizeCommits( - String repoName, - String repoDirName, - String targetRemoteUrl, - Consumer jgitRepoAction - ) throws SynchronizeCommitsException { - - LOGGER.debug("Synchronizing commits for repo {}", repoName); - - boolean reclone = false; - - // Check whether the repo still exists - // Check whether the repo is a valid git repo - // Check whether the remote url is still correct - // If any of the above checks fail, reclone the repo. - try (Repository jgitRepo = repoStorage.acquireRepository(repoDirName)) { - // Check if remote url is still correct - String realRemoteUrl = jgitRepo.getConfig().getString("remote", "origin", "url"); - if (!targetRemoteUrl.equals(realRemoteUrl)) { - throw new InvalidRemoteUrlException(realRemoteUrl, targetRemoteUrl); - } - - // Fetch updates - GuickCloning.getInstance().updateBareRepo(jgitRepo.getDirectory().toPath()); - - // And finally, the reason why we're here - jgitRepoAction.accept(jgitRepo); - - } catch (NoSuchRepositoryException e) { - LOGGER.info("No repo {} found, cloning...", repoName); - reclone = true; - - } catch (RepositoryAcquisitionException e) { - LOGGER.info("Failed to acquire repo {} (maybe damaged), recloning...", repoName, e); - reclone = true; - - } catch (InvalidRemoteUrlException e) { - // This reclones the entire repo instead of trying to change the remote url. This should be - // more robust than trying to set the remote url. The extra overhead doesn't really matter as - // this is expected to happen very rarely. - LOGGER.info("Repo {} has wrong remote url, recloning...", repoName); - LOGGER.debug("real url: {}, target url: {}", e.getRealRemoteUrl(), e.getTargetRemoteUrl()); - reclone = true; - - } catch (CloneException e) { - LOGGER.info("Failed to fetch repo {} (maybe damaged), recloning...", repoName, e); - reclone = true; - } - - if (reclone) { - // Delete the repo, clone it again and try to update the db from the newly cloned repo. - try { - repoStorage.deleteRepository(repoDirName); - repoStorage.addRepository(repoDirName, targetRemoteUrl); - repoStorage.acquireRepository(repoDirName, jgitRepoAction::accept); - - } catch (Exception e) { - LOGGER.warn("Recloning repo {} has failed, possibly because remote url is invalid", - repoName, e); - throw new SynchronizeCommitsException(); - } - } - } - - private void updateCommitsInDb(Repo repo, Repository jgitRepo) { - List toBeQueued = databaseStorage.acquireWriteTransaction(db -> { - return new DbUpdater(repo, jgitRepo, db).update(); - }); - - if (toBeQueued.isEmpty()) { - LOGGER.debug("Adding no new commits to queue"); - } else { - LOGGER.info("Adding " + toBeQueued.size() + " new commits to queue"); - // The commits are ordered from old to new, which means that the new commits will be - // benchmarked first. - queue.addCommits(QUEUE_AUTHOR, repo.getId(), toBeQueued, TASK_PRIORITY); - } - } - - /** - * Run the ANALYZE command. It's relatively quick, so it should be fine to just run it everytime - * the listener runs. - */ - @Timed(histogram = true) - private void runAnalyze() { - LOGGER.debug("Running ANALYZE"); - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl().execute("ANALYZE"); - } - } - - /** - * Run the VACUUM command. Compared to ANALYZE, this one may take a while and copy data around, so - * it is only ran occasionally. - */ - @Timed(histogram = true) - private void vacuumIfNecessary() { - Instant now = Instant.now(); - - Duration timeSinceLastVacuum = Duration.between(lastVacuum, now); - if (timeSinceLastVacuum.compareTo(vacuumInterval) >= 0) { - LOGGER.debug("Running VACUUM"); - - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.vacuum(); - lastVacuum = now; - } - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(Listener.class); + private static final String QUEUE_AUTHOR = "Listener"; + private static final TaskPriority TASK_PRIORITY = TaskPriority.LISTENER; + + private final DatabaseStorage databaseStorage; + private final RepoStorage repoStorage; + + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final RepoWriteAccess repoAccess; + private final BenchRepo benchRepo; + private final SignificanceDetector significanceDetector; + private final Queue queue; + + private final Duration vacuumInterval; + private Instant lastVacuum; + + private final String frontendUrl; + + // An explicit reference to the executor is kept to ensure it will never accidentally be garbage + // collected. This might not be strictly necessary, but it doesn't hurt either. + @SuppressWarnings("FieldCanBeLocal") + private final ScheduledExecutorService executor; + + /** + * Constructs a new listener instance. + * + * @param databaseStorage used to read and write various kinds of data + * @param repoStorage used to manipulate locally cloned repos + * @param repoAccess used to read repo data + * @param benchRepo used to keep the bench repo up-to-date + * @param queue used to add new commits to the queue + * @param pollInterval the time the listener waits between updating its repos + */ + public Listener( + DatabaseStorage databaseStorage, + RepoStorage repoStorage, + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + RepoWriteAccess repoAccess, + BenchRepo benchRepo, + SignificanceDetector significanceDetector, + Queue queue, + Duration pollInterval, + Duration vacuumInterval, + String frontendUrl) { + + this.databaseStorage = databaseStorage; + this.repoStorage = repoStorage; + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.repoAccess = repoAccess; + this.benchRepo = benchRepo; + this.significanceDetector = significanceDetector; + this.queue = queue; + + this.vacuumInterval = vacuumInterval; + this.lastVacuum = Instant.now(); + + this.frontendUrl = frontendUrl; + + executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(this::onUpdate, 0, pollInterval.toSeconds(), TimeUnit.SECONDS); + } + + /** This function is called regularly by the executor. */ + private void onUpdate() { + updateAllRepos(); + runAnalyze(); + vacuumIfNecessary(); + } + + /** + * Re-fetch and update the bench repo and all repos that are in the db. Remove all local clones of + * repos that are no longer in the db and (re-)clone all repos that are not cloned or whose clones + * are broken. + */ + @Timed(histogram = true) + public synchronized void updateAllRepos() { + LOGGER.info("Updating all repos"); + + tryToSynchronizeCommitsForBenchRepo(); + + List currentRepos = repoAccess.getAllRepos(); + tryToDeleteLocalReposExcept(currentRepos); + + for (Repo repo : currentRepos) { + tryToUpdateRepo(repo); + } + } + + private void tryToSynchronizeCommitsForBenchRepo() { + try { + LOGGER.debug("Synchronizing bench repo"); + synchronizeCommitsForBenchRepo(); + } catch (Exception e) { + LOGGER.warn("Failed to synchronize bench repo", e); + } + } + + private void synchronizeCommitsForBenchRepo() throws SynchronizeCommitsException { + genericSynchronizeCommits( + "benchrepo", + benchRepo.getDirName(), + benchRepo.getRemoteUrl(), + jgitRepo -> { + /* The bench repo commits are not stored in the db. */ + }); + } + + private void tryToDeleteLocalReposExcept(List reposToKeep) { + try { + LOGGER.debug("Cleaning up old repos"); + deleteLocalReposExcept(reposToKeep); + } catch (IOException e) { + LOGGER.warn("Failed to clean up old repos", e); + } + } + + private void deleteLocalReposExcept(List reposToKeep) throws IOException { + Set reposThatShouldExist = + Stream.concat( + Stream.of(benchRepo.getDirName()), + reposToKeep.stream().map(Repo::getId).map(RepoId::getDirectoryName)) + .collect(toSet()); + + Set reposThatShouldNotExist = + repoStorage.getRepoDirectories().stream() + .map(Path::getFileName) + .map(Path::toString) + .filter(name -> !reposThatShouldExist.contains(name)) + .collect(toSet()); + + for (String repoName : reposThatShouldNotExist) { + LOGGER.info("Deleting old repo {}", repoName); + repoStorage.deleteRepository(repoName); + } + } + + private void tryToUpdateRepo(Repo repo) { + try { + LOGGER.debug("Updating repo {}", repo.getId()); + updateRepo(repo); + } catch (GithubApiError e) { + LOGGER.warn("Failed to update repo {}", repo.getId()); + LOGGER.warn("{}", e.getMessage()); + } catch (Exception e) { + LOGGER.warn("Failed to update repo {}", repo.getId(), e); + } + } + + private void updateRepo(Repo repo) + throws SynchronizeCommitsException, + IOException, + InterruptedException, + URISyntaxException, + GithubApiError { + + Optional ghIntOpt = + GithubPrInteractor.fromRepo( + repo, + databaseStorage, + benchmarkAccess, + commitAccess, + dimensionAccess, + significanceDetector, + queue, + frontendUrl); + + if (ghIntOpt.isPresent()) { + GithubPrInteractor ghInteractor = ghIntOpt.get(); + + ghInteractor.searchForNewPrCommands(); + + synchronizeCommitsForRepo(repo); + + ghInteractor.markNewPrCommandsAsSeen(); + ghInteractor.addNewPrCommandsToQueue(); + ghInteractor.replyToFinishedPrCommands(); + ghInteractor.replyToErroredPrCommands(); + } else { + synchronizeCommitsForRepo(repo); + } + } + + /** + * Fetch latest commits for repo, store them in the database and add them to the queue if + * necessary. + * + * @throws SynchronizeCommitsException if any of these steps didn't work. This might be because + * the remote url is invalid or inaccessible. + */ + @Timed(histogram = true) + public void synchronizeCommitsForRepo(Repo repo) throws SynchronizeCommitsException { + genericSynchronizeCommits( + repo.getName() + " (" + repo.getIdAsString() + ")", + repo.getId().getDirectoryName(), + repo.getRemoteUrlAsString(), + jgitRepo -> updateCommitsInDb(repo, jgitRepo)); + } + + /** + * Abstracts away updating a repo so the same updating logic can be applied to the bench repo. + * + * @param repoName name of the repo, used only for logging + * @param repoDirName name of the repo's directory. This is not just a {@link RepoId} because this + * function should also work for the bench repo. + * @param targetRemoteUrl the remote url the repo should have. The repo is recloned if its actual + * remote url doesn't match this target remote url. + * @param jgitRepoAction this function will be called on the jgit {@link Repository} if cloning or + * recloning was successful + * @throws SynchronizeCommitsException if the commits in the db could not be updated successfully. + * This may be because the remote url is invalid or inaccessible. + */ + private synchronized void genericSynchronizeCommits( + String repoName, + String repoDirName, + String targetRemoteUrl, + Consumer jgitRepoAction) + throws SynchronizeCommitsException { + + LOGGER.debug("Synchronizing commits for repo {}", repoName); + + boolean reclone = false; + + // Check whether the repo still exists + // Check whether the repo is a valid git repo + // Check whether the remote url is still correct + // If any of the above checks fail, reclone the repo. + try (Repository jgitRepo = repoStorage.acquireRepository(repoDirName)) { + // Check if remote url is still correct + String realRemoteUrl = jgitRepo.getConfig().getString("remote", "origin", "url"); + if (!targetRemoteUrl.equals(realRemoteUrl)) { + throw new InvalidRemoteUrlException(realRemoteUrl, targetRemoteUrl); + } + + // Fetch updates + GuickCloning.getInstance().updateBareRepo(jgitRepo.getDirectory().toPath()); + + // And finally, the reason why we're here + jgitRepoAction.accept(jgitRepo); + + } catch (NoSuchRepositoryException e) { + LOGGER.info("No repo {} found, cloning...", repoName); + reclone = true; + + } catch (RepositoryAcquisitionException e) { + LOGGER.info("Failed to acquire repo {} (maybe damaged), recloning...", repoName, e); + reclone = true; + + } catch (InvalidRemoteUrlException e) { + // This reclones the entire repo instead of trying to change the remote url. This should be + // more robust than trying to set the remote url. The extra overhead doesn't really matter as + // this is expected to happen very rarely. + LOGGER.info("Repo {} has wrong remote url, recloning...", repoName); + LOGGER.debug("real url: {}, target url: {}", e.getRealRemoteUrl(), e.getTargetRemoteUrl()); + reclone = true; + + } catch (CloneException e) { + LOGGER.info("Failed to fetch repo {} (maybe damaged), recloning...", repoName, e); + reclone = true; + } + + if (reclone) { + // Delete the repo, clone it again and try to update the db from the newly cloned repo. + try { + repoStorage.deleteRepository(repoDirName); + repoStorage.addRepository(repoDirName, targetRemoteUrl); + repoStorage.acquireRepository(repoDirName, jgitRepoAction::accept); + + } catch (Exception e) { + LOGGER.warn( + "Recloning repo {} has failed, possibly because remote url is invalid", repoName, e); + throw new SynchronizeCommitsException(); + } + } + } + + private void updateCommitsInDb(Repo repo, Repository jgitRepo) { + List toBeQueued = + databaseStorage.acquireWriteTransaction( + db -> { + return new DbUpdater(repo, jgitRepo, db).update(); + }); + + if (toBeQueued.isEmpty()) { + LOGGER.debug("Adding no new commits to queue"); + } else { + LOGGER.info("Adding " + toBeQueued.size() + " new commits to queue"); + // The commits are ordered from old to new, which means that the new commits will be + // benchmarked first. + queue.addCommits(QUEUE_AUTHOR, repo.getId(), toBeQueued, TASK_PRIORITY); + } + } + + /** + * Run the ANALYZE command. It's relatively quick, so it should be fine to just run it everytime + * the listener runs. + */ + @Timed(histogram = true) + private void runAnalyze() { + LOGGER.debug("Running ANALYZE"); + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().execute("ANALYZE"); + } + } + + /** + * Run the VACUUM command. Compared to ANALYZE, this one may take a while and copy data around, so + * it is only ran occasionally. + */ + @Timed(histogram = true) + private void vacuumIfNecessary() { + Instant now = Instant.now(); + + Duration timeSinceLastVacuum = Duration.between(lastVacuum, now); + if (timeSinceLastVacuum.compareTo(vacuumInterval) >= 0) { + LOGGER.debug("Running VACUUM"); + + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.vacuum(); + lastVacuum = now; + } + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/SynchronizeCommitsException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/SynchronizeCommitsException.java index c21767824..f0e26149c 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/SynchronizeCommitsException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/SynchronizeCommitsException.java @@ -2,6 +2,5 @@ public class SynchronizeCommitsException extends Exception { - public SynchronizeCommitsException() { - } + public SynchronizeCommitsException() {} } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdateException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdateException.java index c9333ec80..0e1e1611a 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdateException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdateException.java @@ -1,11 +1,9 @@ package de.aaaaaaah.velcom.backend.listener.commits; -/** - * An exception thrown when the {@link DbUpdater} failed to update the database for a repo. - */ +/** An exception thrown when the {@link DbUpdater} failed to update the database for a repo. */ public class DbUpdateException extends Exception { - public DbUpdateException(String message, Throwable cause) { - super(message, cause); - } + public DbUpdateException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdater.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdater.java index bd2048d03..a850e5195 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdater.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/commits/DbUpdater.java @@ -26,344 +26,346 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * For a single repo, update the db to mirror the actual git repo's branches and commits. - */ +/** For a single repo, update the db to mirror the actual git repo's branches and commits. */ public class DbUpdater { - private static final Logger LOGGER = LoggerFactory.getLogger(DbUpdater.class); - - private final Repo repo; - private final Repository jgitRepo; - private final DBWriteAccess db; - - private final String repoIdStr; - - public DbUpdater(Repo repo, Repository jgitRepo, DBWriteAccess db) { - this.repo = repo; - this.jgitRepo = jgitRepo; - this.db = db; - - repoIdStr = repo.getIdAsString(); - } - - /** - * Update the DB to reflect the current state of the jgit repo. If the db contains no commits, the - * repo is considered to be new. (This leads to an edge case where a known but empty repo is also - * considered new. However, the worst thing that happens is hat the repo just skips a few of the - * first commits that appear. There are situations where this is good and situations where it is - * slightly annoying, but it's not bad per se.) - * - * @return the hashes of all commits that need to be entered into the queue, sorted by committer - * date (or author date in case of ties) in ascending order (except when this is the repo's first - * update, in which case the order is not defined). These might (but should usually not) include - * commits that are already in the queue. - * @throws DbUpdateException if the db could not be updated for a repo. Other repos may have - * already been updated successfully. - */ - public List update() throws DbUpdateException { - try { - // Checking this now since #insertAllUnknownCommits() will change the result - boolean anyCommits = anyCommits(); - - insertAllUnknownCommits(); - updateBranches(); - updateReachableFlags(); - - List toBeQueued; - if (anyCommits) { - LOGGER.debug("Detected non-empty repository"); - toBeQueued = findNewTasks(); - } else { - LOGGER.debug("Detected empty (i. e. new) repository"); - trackMainBranch(); - toBeQueued = getHeadsOfTrackedBranches(); - } - - updateTrackedFlags(); - - return toBeQueued; - } catch (GitAPIException | IOException e) { - throw new DbUpdateException("Failed to update repo " + repo, e); - } - } - - private KnownCommitRecord jgitCommitToKnownCommitRecord(JgitCommit jgitCommit) { - return new KnownCommitRecord( - repoIdStr, - jgitCommit.getHashAsString(), - false, - false, - false, - jgitCommit.getAuthor(), - jgitCommit.getAuthorDate(), - jgitCommit.getCommitter(), - jgitCommit.getCommitterDate(), - jgitCommit.getMessage() - ); - } - - private List jgitCommitToCommRelRecords(JgitCommit jgitCommit) { - return jgitCommit.getParentHashes().stream() - .map(parentHash -> { - CommitRelationshipRecord record = COMMIT_RELATIONSHIP.newRecord(); - record.setRepoId(repoIdStr); - record.setChildHash(jgitCommit.getHashAsString()); - record.setParentHash(parentHash.getHash()); - return record; - }) - .collect(toList()); - } - - private boolean anyCommits() { - return db.dsl() - .selectFrom(KNOWN_COMMIT) - .where(KNOWN_COMMIT.REPO_ID.eq(repoIdStr)) - .limit(1) - .fetchOptional() - .isPresent(); - } - - /** - * Search through all commits in the jgit repo and add all those to the db that don't already - * exist (including parent relationships). - * - * @throws IOException if jgit does (not sure when that happens) - * @throws GitAPIException if jgit does (not sure when that happens) - */ - private void insertAllUnknownCommits() throws IOException, GitAPIException { - LOGGER.debug("Inserting all unknown commits"); - - // See comment in #insertNewCommits() - Set knownCommits = db.dsl() - .selectFrom(KNOWN_COMMIT) - .where(KNOWN_COMMIT.REPO_ID.eq(repoIdStr)) - .fetchSet(KNOWN_COMMIT.HASH); - - List recordsToInsert = new ArrayList<>(); - List relationshipsToInsert = new ArrayList<>(); - - try (JgitCommitWalk walk = new JgitCommitWalk(jgitRepo)) { - walk.getAllCommits() - .filter(commit -> !knownCommits.contains(commit.getHashAsString())) - .forEach(jgitCommit -> { - recordsToInsert.add(jgitCommitToKnownCommitRecord(jgitCommit)); - relationshipsToInsert.addAll(jgitCommitToCommRelRecords(jgitCommit)); - }); - } - - // Inserting the commits first so we don't violate any foreign key constraints - db.dsl().batchInsert(recordsToInsert).execute(); - db.dsl().batchInsert(relationshipsToInsert).execute(); - } - - /** - * Update the "reachable" field for all commits. It is set to true if the commit is reachable from - * any branch, false otherwise. Works similar to {@link #updateTrackedFlags()}. - */ - private void updateReachableFlags() { - LOGGER.debug("Updating reachable flags"); - - String query = "" - + "WITH RECURSIVE rec(hash) AS (\n" - + " SELECT branch.latest_commit_hash\n" - + " FROM branch\n" - + " WHERE branch.repo_id = ?\n" // <-- Binding #1 - + " \n" - + " UNION\n" - + " \n" - + " SELECT commit_relationship.parent_hash\n" - + " FROM commit_relationship\n" - + " JOIN rec\n" - + " ON rec.hash = commit_relationship.child_hash\n" - + ")\n" - + "\n" - + "UPDATE known_commit\n" - + "SET reachable = (known_commit.hash IN rec)\n" - + "WHERE known_commit.repo_id = ?\n" // <-- Binding #2 - + ""; - - db.dsl().execute(query, repoIdStr, repoIdStr); - } - - /** - * Update the "tracked" field for all commits. It is set to true if the commit is reachable from a - * tracked branch, false otherwise. This is accomplished with a nifty recursive query that - * hopefully works as intended and is not too resource intensive. - */ - private void updateTrackedFlags() { - LOGGER.debug("Updating tracked flags"); - - String query = "" - + "WITH RECURSIVE rec(hash) AS (\n" - + " SELECT branch.latest_commit_hash\n" - + " FROM branch\n" - + " WHERE branch.repo_id = ?\n" // <-- Binding #1 - + " AND branch.tracked\n" - + " \n" - + " UNION\n" - + " \n" - + " SELECT commit_relationship.parent_hash\n" - + " FROM commit_relationship\n" - + " JOIN rec\n" - + " ON rec.hash = commit_relationship.child_hash\n" - + ")\n" - + "\n" - + "UPDATE known_commit\n" - + "SET\n" - + " tracked = (known_commit.hash IN rec),\n" - + " ever_tracked = ever_tracked OR (known_commit.hash IN rec)\n" - + "WHERE known_commit.repo_id = ?\n" // <-- Binding #2 - + ""; - - db.dsl().execute(query, repoIdStr, repoIdStr); - } - - /** - * Update the "branch" table with the repo's current branches. - * - * @throws GitAPIException if jgit fails somehow - */ - private void updateBranches() throws GitAPIException { - LOGGER.debug("Updating branches"); - - Set trackedBranchNames = db.dsl() - .selectFrom(BRANCH) - .where(BRANCH.REPO_ID.eq(repoIdStr)) - .and(BRANCH.TRACKED) - .stream() - .map(record -> BranchName.fromName(record.getName())) - .collect(toSet()); - - List branchRecords = new Git(jgitRepo).branchList().call().stream() - .map(ref -> { - BranchName name = BranchName.fromFullName(ref.getName()); - String latestCommitHash = ref.getObjectId().getName(); - - BranchRecord record = BRANCH.newRecord(); - record.setRepoId(repoIdStr); - record.setName(name.getName()); - record.setLatestCommitHash(latestCommitHash); - record.setTracked(trackedBranchNames.contains(name)); - return record; - }) - .collect(toList()); - - db.dsl().deleteFrom(BRANCH).where(BRANCH.REPO_ID.eq(repoIdStr)).execute(); - db.dsl().batchInsert(branchRecords).execute(); - } - - /** - * Find all commits that should be tracked (via a recursive query) and add them to the queue (if - * they haven't already been benchmarked yet). - *

- * This query only considers commits with the "ever_tracked" flag set to false. It starts at the - * tips of the tracked branches and then finds all connected commits. It always moves from child - * to parent, never from parent to child. - *

- * In other words, consider a directed graph of all commits with "ever_tracked" set to false. The - * edges are child-parent relationships (arrows pointing from child to parent). This query finds - * all nodes (i. e. commits) that are reachable from the tips of the tracked branches (should they - * be present in the graph). - *

- * The "ever_tracked" flag is used instead of the "tracked" flag to avoid filling the queue with - * old commits when marking a branch tracked in a repo with no tracked branches but lots of - * commits. - * - * @return the hashes of all commits that need to be entered into the queue, sorted by committer - * date (or author date in case of ties) in ascending order. These may include commits that are - * already in the queue. - */ - private List findNewTasks() { - LOGGER.debug("Finding new tasks"); - - // The "untracked" CTE will only ever include reachable commits since we start at branches. This - // means we don't have to check the "reachable" flag in the query. - String query = "" - + "WITH RECURSIVE\n" - + "\n" - + "untracked(hash) AS (\n" - + " SELECT branch.latest_commit_hash\n" - + " FROM branch\n" - + " JOIN known_commit\n" - + " ON known_commit.repo_id = branch.repo_id\n" - + " AND known_commit.hash = branch.latest_commit_hash\n" - + " WHERE branch.repo_id = ?\n" // <-- Binding #1 - + " AND branch.tracked\n" - + " AND NOT known_commit.ever_tracked\n" - + " \n" - + " UNION\n" - + " \n" - + " SELECT known_commit.hash\n" - + " FROM known_commit\n" - + " JOIN commit_relationship\n" - + " ON commit_relationship.parent_hash = known_commit.hash\n" - + " JOIN untracked\n" - + " ON untracked.hash = commit_relationship.child_hash\n" - + " WHERE known_commit.repo_id = ?\n" // <-- Binding #2 - + " AND NOT known_commit.ever_tracked\n" - + "),\n" - + "\n" - + "has_result(hash) AS (\n" - + " SELECT DISTINCT run.commit_hash\n" - + " FROM run\n" - + " WHERE run.repo_id = ?\n" // <-- Binding #3 - + " AND run.commit_hash IS NOT NULL\n" - + "),\n" - + "\n" - + "in_queue(hash) AS (\n" - + " SELECT DISTINCT task.commit_hash\n" - + " FROM task\n" - + " WHERE task.repo_id = ?\n" // <-- Binding #4 - + " AND task.commit_hash IS NOT NULL\n" - + ")\n" - + "\n" - + "SELECT DISTINCT untracked.hash\n" - + "FROM untracked\n" - + "JOIN known_commit\n" - + " ON known_commit.hash = untracked.hash\n" - + "WHERE untracked.hash NOT IN has_result\n" - + "AND untracked.hash NOT IN in_queue\n" - + "ORDER BY known_commit.committer_date ASC, known_commit.author_date ASC\n" - + ""; - - return db.dsl() - .fetchLazy(query, repoIdStr, repoIdStr, repoIdStr, repoIdStr) - .stream() - .map(record -> (String) record.getValue(0)) - .map(CommitHash::new) - .collect(toList()); - } - - /** - * Track the repo's main branch (if one exists). - */ - private void trackMainBranch() throws IOException { - Optional.ofNullable(jgitRepo.getFullBranch()) - .map(BranchName::fromFullName) - .ifPresent(branchName -> db.dsl() - .update(BRANCH) - .set(BRANCH.TRACKED, true) - .where(BRANCH.REPO_ID.eq(repoIdStr)) - .and(BRANCH.NAME.eq(branchName.getName())) - .execute()); - } - - /** - * @return the hashes of all commits that need to be entered into the queue, in no particular - * order. In theory, these may include commits that are already in the queue and even commits - * that have already been benchmarked. In practice, this function should only be called for new - * repos, so the queue should not yet contain any of the repo's commits. - */ - private List getHeadsOfTrackedBranches() { - // Similar to #findNewTasks(), this function won't ever find unreachable commits. - return db.dsl() - .selectFrom(BRANCH) - .where(BRANCH.REPO_ID.eq(repoIdStr)) - .and(BRANCH.TRACKED) - .stream() - .map(BranchRecord::getLatestCommitHash) - .map(CommitHash::new) - .collect(toList()); - } + private static final Logger LOGGER = LoggerFactory.getLogger(DbUpdater.class); + + private final Repo repo; + private final Repository jgitRepo; + private final DBWriteAccess db; + + private final String repoIdStr; + + public DbUpdater(Repo repo, Repository jgitRepo, DBWriteAccess db) { + this.repo = repo; + this.jgitRepo = jgitRepo; + this.db = db; + + repoIdStr = repo.getIdAsString(); + } + + /** + * Update the DB to reflect the current state of the jgit repo. If the db contains no commits, the + * repo is considered to be new. (This leads to an edge case where a known but empty repo is also + * considered new. However, the worst thing that happens is hat the repo just skips a few of the + * first commits that appear. There are situations where this is good and situations where it is + * slightly annoying, but it's not bad per se.) + * + * @return the hashes of all commits that need to be entered into the queue, sorted by committer + * date (or author date in case of ties) in ascending order (except when this is the repo's + * first update, in which case the order is not defined). These might (but should usually not) + * include commits that are already in the queue. + * @throws DbUpdateException if the db could not be updated for a repo. Other repos may have + * already been updated successfully. + */ + public List update() throws DbUpdateException { + try { + // Checking this now since #insertAllUnknownCommits() will change the result + boolean anyCommits = anyCommits(); + + insertAllUnknownCommits(); + updateBranches(); + updateReachableFlags(); + + List toBeQueued; + if (anyCommits) { + LOGGER.debug("Detected non-empty repository"); + toBeQueued = findNewTasks(); + } else { + LOGGER.debug("Detected empty (i. e. new) repository"); + trackMainBranch(); + toBeQueued = getHeadsOfTrackedBranches(); + } + + updateTrackedFlags(); + + return toBeQueued; + } catch (GitAPIException | IOException e) { + throw new DbUpdateException("Failed to update repo " + repo, e); + } + } + + private KnownCommitRecord jgitCommitToKnownCommitRecord(JgitCommit jgitCommit) { + return new KnownCommitRecord( + repoIdStr, + jgitCommit.getHashAsString(), + false, + false, + false, + jgitCommit.getAuthor(), + jgitCommit.getAuthorDate(), + jgitCommit.getCommitter(), + jgitCommit.getCommitterDate(), + jgitCommit.getMessage()); + } + + private List jgitCommitToCommRelRecords(JgitCommit jgitCommit) { + return jgitCommit.getParentHashes().stream() + .map( + parentHash -> { + CommitRelationshipRecord record = COMMIT_RELATIONSHIP.newRecord(); + record.setRepoId(repoIdStr); + record.setChildHash(jgitCommit.getHashAsString()); + record.setParentHash(parentHash.getHash()); + return record; + }) + .collect(toList()); + } + + private boolean anyCommits() { + return db.dsl() + .selectFrom(KNOWN_COMMIT) + .where(KNOWN_COMMIT.REPO_ID.eq(repoIdStr)) + .limit(1) + .fetchOptional() + .isPresent(); + } + + /** + * Search through all commits in the jgit repo and add all those to the db that don't already + * exist (including parent relationships). + * + * @throws IOException if jgit does (not sure when that happens) + * @throws GitAPIException if jgit does (not sure when that happens) + */ + private void insertAllUnknownCommits() throws IOException, GitAPIException { + LOGGER.debug("Inserting all unknown commits"); + + // See comment in #insertNewCommits() + Set knownCommits = + db.dsl() + .selectFrom(KNOWN_COMMIT) + .where(KNOWN_COMMIT.REPO_ID.eq(repoIdStr)) + .fetchSet(KNOWN_COMMIT.HASH); + + List recordsToInsert = new ArrayList<>(); + List relationshipsToInsert = new ArrayList<>(); + + try (JgitCommitWalk walk = new JgitCommitWalk(jgitRepo)) { + walk.getAllCommits() + .filter(commit -> !knownCommits.contains(commit.getHashAsString())) + .forEach( + jgitCommit -> { + recordsToInsert.add(jgitCommitToKnownCommitRecord(jgitCommit)); + relationshipsToInsert.addAll(jgitCommitToCommRelRecords(jgitCommit)); + }); + } + + // Inserting the commits first so we don't violate any foreign key constraints + db.dsl().batchInsert(recordsToInsert).execute(); + db.dsl().batchInsert(relationshipsToInsert).execute(); + } + + /** + * Update the "reachable" field for all commits. It is set to true if the commit is reachable from + * any branch, false otherwise. Works similar to {@link #updateTrackedFlags()}. + */ + private void updateReachableFlags() { + LOGGER.debug("Updating reachable flags"); + + String query = + "" + + "WITH RECURSIVE rec(hash) AS (\n" + + " SELECT branch.latest_commit_hash\n" + + " FROM branch\n" + + " WHERE branch.repo_id = ?\n" // <-- Binding #1 + + " \n" + + " UNION\n" + + " \n" + + " SELECT commit_relationship.parent_hash\n" + + " FROM commit_relationship\n" + + " JOIN rec\n" + + " ON rec.hash = commit_relationship.child_hash\n" + + ")\n" + + "\n" + + "UPDATE known_commit\n" + + "SET reachable = (known_commit.hash IN rec)\n" + + "WHERE known_commit.repo_id = ?\n" // <-- Binding #2 + + ""; + + db.dsl().execute(query, repoIdStr, repoIdStr); + } + + /** + * Update the "tracked" field for all commits. It is set to true if the commit is reachable from a + * tracked branch, false otherwise. This is accomplished with a nifty recursive query that + * hopefully works as intended and is not too resource intensive. + */ + private void updateTrackedFlags() { + LOGGER.debug("Updating tracked flags"); + + String query = + "" + + "WITH RECURSIVE rec(hash) AS (\n" + + " SELECT branch.latest_commit_hash\n" + + " FROM branch\n" + + " WHERE branch.repo_id = ?\n" // <-- Binding #1 + + " AND branch.tracked\n" + + " \n" + + " UNION\n" + + " \n" + + " SELECT commit_relationship.parent_hash\n" + + " FROM commit_relationship\n" + + " JOIN rec\n" + + " ON rec.hash = commit_relationship.child_hash\n" + + ")\n" + + "\n" + + "UPDATE known_commit\n" + + "SET\n" + + " tracked = (known_commit.hash IN rec),\n" + + " ever_tracked = ever_tracked OR (known_commit.hash IN rec)\n" + + "WHERE known_commit.repo_id = ?\n" // <-- Binding #2 + + ""; + + db.dsl().execute(query, repoIdStr, repoIdStr); + } + + /** + * Update the "branch" table with the repo's current branches. + * + * @throws GitAPIException if jgit fails somehow + */ + private void updateBranches() throws GitAPIException { + LOGGER.debug("Updating branches"); + + Set trackedBranchNames = + db.dsl().selectFrom(BRANCH).where(BRANCH.REPO_ID.eq(repoIdStr)).and(BRANCH.TRACKED).stream() + .map(record -> BranchName.fromName(record.getName())) + .collect(toSet()); + + List branchRecords = + new Git(jgitRepo) + .branchList().call().stream() + .map( + ref -> { + BranchName name = BranchName.fromFullName(ref.getName()); + String latestCommitHash = ref.getObjectId().getName(); + + BranchRecord record = BRANCH.newRecord(); + record.setRepoId(repoIdStr); + record.setName(name.getName()); + record.setLatestCommitHash(latestCommitHash); + record.setTracked(trackedBranchNames.contains(name)); + return record; + }) + .collect(toList()); + + db.dsl().deleteFrom(BRANCH).where(BRANCH.REPO_ID.eq(repoIdStr)).execute(); + db.dsl().batchInsert(branchRecords).execute(); + } + + /** + * Find all commits that should be tracked (via a recursive query) and add them to the queue (if + * they haven't already been benchmarked yet). + * + *

This query only considers commits with the "ever_tracked" flag set to false. It starts at + * the tips of the tracked branches and then finds all connected commits. It always moves from + * child to parent, never from parent to child. + * + *

In other words, consider a directed graph of all commits with "ever_tracked" set to false. + * The edges are child-parent relationships (arrows pointing from child to parent). This query + * finds all nodes (i. e. commits) that are reachable from the tips of the tracked branches + * (should they be present in the graph). + * + *

The "ever_tracked" flag is used instead of the "tracked" flag to avoid filling the queue + * with old commits when marking a branch tracked in a repo with no tracked branches but lots of + * commits. + * + * @return the hashes of all commits that need to be entered into the queue, sorted by committer + * date (or author date in case of ties) in ascending order. These may include commits that + * are already in the queue. + */ + private List findNewTasks() { + LOGGER.debug("Finding new tasks"); + + // The "untracked" CTE will only ever include reachable commits since we start at branches. This + // means we don't have to check the "reachable" flag in the query. + String query = + "" + + "WITH RECURSIVE\n" + + "\n" + + "untracked(hash) AS (\n" + + " SELECT branch.latest_commit_hash\n" + + " FROM branch\n" + + " JOIN known_commit\n" + + " ON known_commit.repo_id = branch.repo_id\n" + + " AND known_commit.hash = branch.latest_commit_hash\n" + + " WHERE branch.repo_id = ?\n" // <-- Binding #1 + + " AND branch.tracked\n" + + " AND NOT known_commit.ever_tracked\n" + + " \n" + + " UNION\n" + + " \n" + + " SELECT known_commit.hash\n" + + " FROM known_commit\n" + + " JOIN commit_relationship\n" + + " ON commit_relationship.parent_hash = known_commit.hash\n" + + " JOIN untracked\n" + + " ON untracked.hash = commit_relationship.child_hash\n" + + " WHERE known_commit.repo_id = ?\n" // <-- Binding #2 + + " AND NOT known_commit.ever_tracked\n" + + "),\n" + + "\n" + + "has_result(hash) AS (\n" + + " SELECT DISTINCT run.commit_hash\n" + + " FROM run\n" + + " WHERE run.repo_id = ?\n" // <-- Binding #3 + + " AND run.commit_hash IS NOT NULL\n" + + "),\n" + + "\n" + + "in_queue(hash) AS (\n" + + " SELECT DISTINCT task.commit_hash\n" + + " FROM task\n" + + " WHERE task.repo_id = ?\n" // <-- Binding #4 + + " AND task.commit_hash IS NOT NULL\n" + + ")\n" + + "\n" + + "SELECT DISTINCT untracked.hash\n" + + "FROM untracked\n" + + "JOIN known_commit\n" + + " ON known_commit.hash = untracked.hash\n" + + "WHERE untracked.hash NOT IN has_result\n" + + "AND untracked.hash NOT IN in_queue\n" + + "ORDER BY known_commit.committer_date ASC, known_commit.author_date ASC\n" + + ""; + + return db.dsl().fetchLazy(query, repoIdStr, repoIdStr, repoIdStr, repoIdStr).stream() + .map(record -> (String) record.getValue(0)) + .map(CommitHash::new) + .collect(toList()); + } + + /** Track the repo's main branch (if one exists). */ + private void trackMainBranch() throws IOException { + Optional.ofNullable(jgitRepo.getFullBranch()) + .map(BranchName::fromFullName) + .ifPresent( + branchName -> + db.dsl() + .update(BRANCH) + .set(BRANCH.TRACKED, true) + .where(BRANCH.REPO_ID.eq(repoIdStr)) + .and(BRANCH.NAME.eq(branchName.getName())) + .execute()); + } + + /** + * @return the hashes of all commits that need to be entered into the queue, in no particular + * order. In theory, these may include commits that are already in the queue and even commits + * that have already been benchmarked. In practice, this function should only be called for + * new repos, so the queue should not yet contain any of the repo's commits. + */ + private List getHeadsOfTrackedBranches() { + // Similar to #findNewTasks(), this function won't ever find unreachable commits. + return db + .dsl() + .selectFrom(BRANCH) + .where(BRANCH.REPO_ID.eq(repoIdStr)) + .and(BRANCH.TRACKED) + .stream() + .map(BranchRecord::getLatestCommitHash) + .map(CommitHash::new) + .collect(toList()); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/FinishedGithubCommand.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/FinishedGithubCommand.java index 9c35179cb..4e3b0af62 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/FinishedGithubCommand.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/FinishedGithubCommand.java @@ -6,33 +6,33 @@ public class FinishedGithubCommand { - private final long pr; - private final BranchName targetBranch; - private final CommitHash commitHash; - private final RunId runId; - - public FinishedGithubCommand(long pr, BranchName targetBranch, CommitHash commitHash, - RunId runId) { - - this.pr = pr; - this.targetBranch = targetBranch; - this.commitHash = commitHash; - this.runId = runId; - } - - public long getPr() { - return pr; - } - - public BranchName getTargetBranch() { - return targetBranch; - } - - public CommitHash getCommitHash() { - return commitHash; - } - - public RunId getRunId() { - return runId; - } + private final long pr; + private final BranchName targetBranch; + private final CommitHash commitHash; + private final RunId runId; + + public FinishedGithubCommand( + long pr, BranchName targetBranch, CommitHash commitHash, RunId runId) { + + this.pr = pr; + this.targetBranch = targetBranch; + this.commitHash = commitHash; + this.runId = runId; + } + + public long getPr() { + return pr; + } + + public BranchName getTargetBranch() { + return targetBranch; + } + + public CommitHash getCommitHash() { + return commitHash; + } + + public RunId getRunId() { + return runId; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubApiError.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubApiError.java index 5bc01e27e..7ee62584c 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubApiError.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubApiError.java @@ -4,14 +4,14 @@ public class GithubApiError extends Exception { - private final URI url; + private final URI url; - public GithubApiError(String message, URI url) { - super("Error while accessing " + url + ": " + message); - this.url = url; - } + public GithubApiError(String message, URI url) { + super("Error while accessing " + url + ": " + message); + this.url = url; + } - public URI getUrl() { - return url; - } + public URI getUrl() { + return url; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommand.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommand.java index 6a537f710..0fbe958ec 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommand.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommand.java @@ -7,90 +7,105 @@ public class GithubCommand { - private static final int TRIES = 5; - - private final RepoId repoId; - private final long pr; - private final BranchName targetBranch; - private final long comment; - private final CommitHash commitHash; - private final GithubCommandState state; - private final int triesLeft; - - public GithubCommand(RepoId repoId, long pr, BranchName targetBranch, long comment, - CommitHash commitHash, GithubCommandState state, int triesLeft) { - - this.repoId = repoId; - this.pr = pr; - this.targetBranch = targetBranch; - this.comment = comment; - this.commitHash = commitHash; - this.state = state; - this.triesLeft = triesLeft; - } - - public GithubCommand(RepoId repoId, long pr, BranchName targetBranch, long comment, - CommitHash commitHash) { - - this(repoId, pr, targetBranch, comment, commitHash, GithubCommandState.NEW, TRIES); - } - - public RepoId getRepoId() { - return repoId; - } - - public long getPr() { - return pr; - } - - public BranchName getTargetBranch() { - return targetBranch; - } - - public long getComment() { - return comment; - } - - public CommitHash getCommitHash() { - return commitHash; - } - - public GithubCommandState getState() { - return state; - } - - public int getTriesLeft() { - return triesLeft; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GithubCommand that = (GithubCommand) o; - return pr == that.pr && comment == that.comment && triesLeft == that.triesLeft - && Objects.equals(repoId, that.repoId) && Objects - .equals(commitHash, that.commitHash) && state == that.state; - } - - @Override - public int hashCode() { - return Objects.hash(repoId, pr, comment, commitHash, state, triesLeft); - } - - @Override - public String toString() { - return "GithubCommand{" + - "repoId=" + repoId + - ", pr=" + pr + - ", comment=" + comment + - ", commitHash=" + commitHash + - ", state=" + state + - ", triesLeft=" + triesLeft + - '}'; - } + private static final int TRIES = 5; + + private final RepoId repoId; + private final long pr; + private final BranchName targetBranch; + private final long comment; + private final CommitHash commitHash; + private final GithubCommandState state; + private final int triesLeft; + + public GithubCommand( + RepoId repoId, + long pr, + BranchName targetBranch, + long comment, + CommitHash commitHash, + GithubCommandState state, + int triesLeft) { + + this.repoId = repoId; + this.pr = pr; + this.targetBranch = targetBranch; + this.comment = comment; + this.commitHash = commitHash; + this.state = state; + this.triesLeft = triesLeft; + } + + public GithubCommand( + RepoId repoId, long pr, BranchName targetBranch, long comment, CommitHash commitHash) { + + this(repoId, pr, targetBranch, comment, commitHash, GithubCommandState.NEW, TRIES); + } + + public RepoId getRepoId() { + return repoId; + } + + public long getPr() { + return pr; + } + + public BranchName getTargetBranch() { + return targetBranch; + } + + public long getComment() { + return comment; + } + + public CommitHash getCommitHash() { + return commitHash; + } + + public GithubCommandState getState() { + return state; + } + + public int getTriesLeft() { + return triesLeft; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GithubCommand that = (GithubCommand) o; + return pr == that.pr + && comment == that.comment + && triesLeft == that.triesLeft + && Objects.equals(repoId, that.repoId) + && Objects.equals(commitHash, that.commitHash) + && state == that.state; + } + + @Override + public int hashCode() { + return Objects.hash(repoId, pr, comment, commitHash, state, triesLeft); + } + + @Override + public String toString() { + return "GithubCommand{" + + "repoId=" + + repoId + + ", pr=" + + pr + + ", comment=" + + comment + + ", commitHash=" + + commitHash + + ", state=" + + state + + ", triesLeft=" + + triesLeft + + '}'; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommandState.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommandState.java index 8037905b6..bcc4cb777 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommandState.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubCommandState.java @@ -3,30 +3,30 @@ import java.util.List; public enum GithubCommandState { - NEW("NEW"), - MARKED_SEEN("MARKED_SEEN"), - QUEUED("QUEUED"), - ERROR("ERROR"); + NEW("NEW"), + MARKED_SEEN("MARKED_SEEN"), + QUEUED("QUEUED"), + ERROR("ERROR"); - private final String textualRepresentation; + private final String textualRepresentation; - GithubCommandState(String textualRepresentation) { - this.textualRepresentation = textualRepresentation; - } + GithubCommandState(String textualRepresentation) { + this.textualRepresentation = textualRepresentation; + } - public static GithubCommandState fromTextualRepresentation(String representation) - throws IllegalArgumentException { + public static GithubCommandState fromTextualRepresentation(String representation) + throws IllegalArgumentException { - for (GithubCommandState state : List.of(NEW, MARKED_SEEN, QUEUED, ERROR)) { - if (state.getTextualRepresentation().equals(representation)) { - return state; - } - } + for (GithubCommandState state : List.of(NEW, MARKED_SEEN, QUEUED, ERROR)) { + if (state.getTextualRepresentation().equals(representation)) { + return state; + } + } - throw new IllegalArgumentException("\"" + representation + "\" is not a valid command state"); - } + throw new IllegalArgumentException("\"" + representation + "\" is not a valid command state"); + } - public String getTextualRepresentation() { - return textualRepresentation; - } + public String getTextualRepresentation() { + return textualRepresentation; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractor.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractor.java index 39aab40ef..f5044252f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractor.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractor.java @@ -3,7 +3,6 @@ import static java.util.Comparator.comparing; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; import static org.jooq.codegen.db.tables.GithubCommand.GITHUB_COMMAND; import static org.jooq.codegen.db.tables.Repo.REPO; @@ -54,7 +53,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.ws.rs.core.HttpHeaders; @@ -64,750 +62,808 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Interact with the GitHub API to find new !bench commands and respond to existing ones. - */ +/** Interact with the GitHub API to find new !bench commands and respond to existing ones. */ public class GithubPrInteractor { - private static final Logger LOGGER = LoggerFactory.getLogger(GithubPrInteractor.class); - - private final Repo repo; - private final GithubInfo ghInfo; - private final DatabaseStorage databaseStorage; - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final SignificanceDetector significanceDetector; - private final Queue queue; - - private final String frontendUrl; - - private final HttpClient client; - private final JsonBodyHandler jsonBodyHandler; - private final String basicAuthHeader; - - private GithubPrInteractor(Repo repo, GithubInfo ghInfo, DatabaseStorage databaseStorage, - BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess, SignificanceDetector significanceDetector, Queue queue, - String frontendUrl) { - - this.repo = repo; - this.ghInfo = ghInfo; - this.databaseStorage = databaseStorage; - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.significanceDetector = significanceDetector; - this.queue = queue; - - this.frontendUrl = frontendUrl; - - client = HttpClient.newHttpClient(); - jsonBodyHandler = new JsonBodyHandler(); - - String authInfo = ":" + ghInfo.getAccessToken(); - byte[] authInfoBytes = authInfo.getBytes(StandardCharsets.UTF_8); - String authInfoBase64 = Base64.getEncoder().encodeToString(authInfoBytes); - basicAuthHeader = "Basic " + authInfoBase64; - } - - public static Optional fromRepo(Repo repo, DatabaseStorage databaseStorage, - BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess, SignificanceDetector significanceDetector, Queue queue, - String frontendUrl) { - - Optional ghInfoOpt = repo.getGithubInfo(); - return ghInfoOpt.map(githubInfo -> new GithubPrInteractor( - repo, - githubInfo, - databaseStorage, - benchmarkAccess, - commitAccess, - dimensionAccess, - significanceDetector, - queue, - frontendUrl - )); - } - - private static GithubCommand commandRecordToCommand(GithubCommandRecord record) { - return new GithubCommand( - RepoId.fromString(record.getRepoId()), - record.getPr(), - BranchName.fromName(record.getTargetBranch()), - record.getComment(), - new CommitHash(record.getCommitHash()), - GithubCommandState.fromTextualRepresentation(record.getState()), - record.getTriesLeft() - ); - } - - private void checkResponse(HttpResponse response) throws GithubApiError { - if (response.statusCode() < 100 || response.statusCode() >= 300) { - JsonNode message = response.body().get("message"); - if (message == null) { - throw new GithubApiError("Something went wrong", response.uri()); - } else { - throw new GithubApiError(message.toString(), response.uri()); - } - } - } - - private JsonNode getResourceFromUrl(String issueUrl) - throws URISyntaxException, IOException, InterruptedException, GithubApiError { - - URI uri = new URI(issueUrl); - HttpRequest request = HttpRequest.newBuilder(uri) - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) - .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") - .build(); - HttpResponse response = client.send(request, jsonBodyHandler); - checkResponse(response); - return response.body(); - } - - private JsonNode getCommentPage(int page) - throws IOException, InterruptedException, GithubApiError { - - URI uri = UriBuilder.fromUri("https://api.github.com/") - .path("repos") - .path(ghInfo.getRepoName()) - .path("issues/comments") - .queryParam("since", ghInfo.getCommentCutoff().toString()) - .queryParam("sort", "created") - // The direction is important! If we used "asc" instead, we might lose older comments if - // comments are updated in-between fetching different pages. With "desc", we instead see some - // comments multiple times in the same scenario (and we'll be able to get the comments we - // missed on the next run). - .queryParam("direction", "desc") - .queryParam("page", page) - .build(); - HttpRequest request = HttpRequest.newBuilder(uri) - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) - .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") - .build(); - HttpResponse response = client.send(request, jsonBodyHandler); - checkResponse(response); - return response.body(); - } - - private Optional findNewCommandCandidates( - List> commandCandidates) - throws IOException, InterruptedException, URISyntaxException, GithubApiError { - - Set seen = new HashSet<>(); - Instant commentCutoff = ghInfo.getCommentCutoff(); - Optional newCommentCutoff = Optional.empty(); - - for (int page = 1; true; page++) { - LOGGER.debug("Looking at comment page {}", page); - JsonNode comments = getCommentPage(page); - if (comments.isEmpty()) { - LOGGER.debug("Page {} is empty", page); - break; // We've hit the last page - } - - for (JsonNode comment : comments) { - Instant createdAt = Instant.parse(comment.get("created_at").asText()); - if (createdAt.isBefore(commentCutoff) || createdAt.equals(commentCutoff)) { - // The "since" query parameter only cuts off comments that were updated before the - // specified cutoff. However, we don't want to look at any comments *created* before the - // cutoff, so we need to do a bit of extra filtering. - LOGGER.debug("Ignoring comment created before comment cutoff"); - continue; - } - - if (newCommentCutoff.isEmpty()) { - // We know that this comment is the first comment returned by the GitHub API. Since the - // comments are sorted by creation time in descending order, we know that this is the - // newest comment. - newCommentCutoff = Optional.of(createdAt); - } - - long commentId = comment.get("id").asLong(); - if (seen.contains(commentId)) { - // New comments may have appeared since we requested the previous page, leading to - // comments we've already seen being pushed to this (next) page. - LOGGER.debug("Ignoring comment we've already seen"); - continue; - } - seen.add(commentId); - - String commentBody = comment.get("body").asText(); - if (!commentBody.strip().startsWith("!bench")) { - LOGGER.debug("Ignoring comment without command ({})", commentBody); - continue; // Not a command, just a normal comment - } - - JsonNode issue = getResourceFromUrl(comment.get("issue_url").asText()); - if (!issue.has("pull_request")) { - LOGGER.debug("Ignoring issue comment ({})", commentBody); - continue; // Not a PR, just a normal issue - } - - JsonNode pr = getResourceFromUrl(issue.get("pull_request").get("url").asText()); - - long prNumber = pr.get("number").asLong(); - BranchName targetBranch = BranchName.fromName(pr.get("base").get("ref").asText()); - CommitHash commitHash = new CommitHash(pr.get("head").get("sha").asText()); - GithubCommand command = new GithubCommand(repo.getId(), prNumber, targetBranch, commentId, - commitHash); - String username = comment.get("user").get("login").asText(); - commandCandidates.add(new Pair<>(command, username)); - LOGGER.debug("Found command for pr #{} ({}, by {})", prNumber, commentBody, username); - } - } - - return newCommentCutoff; - } - - private List keepOnlyAuthorizedCandidates( - List> commandCandidates) - throws IOException, InterruptedException, GithubApiError { - - Set usernames = commandCandidates.stream() - .map(Pair::getSecond) - .collect(toSet()); - - Set usersWithWritePermission = new HashSet<>(); - for (String username : usernames) { - // HACK: Temporarily disable permission checks - usersWithWritePermission.add(username); - - LOGGER.debug("Checking if {} has write permissions", username); - URI url = UriBuilder.fromUri("https://api.github.com/") - .path("repos") - .path(ghInfo.getRepoName()) - .path("collaborators") - .path(username) - .path("permission") - .build(); - HttpRequest request = HttpRequest.newBuilder(url) - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) - .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") - .build(); - HttpResponse response = client.send(request, jsonBodyHandler); - checkResponse(response); - if (response.statusCode() != 200) { - LOGGER.debug("User {} doesn't have write permissions (not a collaborator)", username); - continue; - } - String permission = response.body().get("permission").asText(); - if (permission.equals("admin") || permission.equals("write")) { - LOGGER.debug("User {} has write permissions ({})", username, permission); - usersWithWritePermission.add(username); - } else { - LOGGER.debug("User {} doesn't have write permissions ({})", username, permission); - } - } - - return commandCandidates.stream() - .filter(pair -> usersWithWritePermission.contains(pair.getSecond())) - .map(Pair::getFirst) - .collect(toList()); - } - - public void searchForNewPrCommands() - throws IOException, InterruptedException, URISyntaxException, GithubApiError { - - LOGGER.debug("Searching for new PR commands"); - - List> commandCandidates = new ArrayList<>(); - Optional newCommentCutoff = findNewCommandCandidates(commandCandidates); - List commands = keepOnlyAuthorizedCandidates(commandCandidates); - - if (newCommentCutoff.isEmpty() || commands.isEmpty()) { - // If we don't have a new cutoff, definitely haven't found any new commands either - return; - } - Instant newCommentCutoffValue = newCommentCutoff.get(); - - databaseStorage.acquireWriteTransaction(db -> { - RepoRecord repoRecord = db.dsl() - .selectFrom(REPO) - .where(REPO.ID.eq(repo.getIdAsString())) - .fetchSingle(); - if (repoRecord.getGithubCommentCutoff() == null) { - return; // While we were crawling, GitHub integration has been turned off - } - repoRecord.setGithubCommentCutoff(newCommentCutoffValue); - db.dsl().batchUpdate(repoRecord).execute(); - - List commandRecords = commands.stream() - .map(command -> new GithubCommandRecord( - command.getRepoId().getIdAsString(), - command.getPr(), - command.getTargetBranch().getName(), - command.getComment(), - command.getCommitHash().getHash(), - command.getState().getTextualRepresentation(), - command.getTriesLeft() - )) - .collect(toList()); - db.dsl().batchInsert(commandRecords).execute(); - }); - } - - public void markNewPrCommandsAsSeen() throws IOException, InterruptedException { - LOGGER.debug("Marking new commands as seen"); - - List newCommands = databaseStorage.acquireReadTransaction(db -> { - return db.dsl() - .selectFrom(GITHUB_COMMAND) - .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) - .and(GITHUB_COMMAND.STATE.eq(GithubCommandState.NEW.getTextualRepresentation())) - .stream() - .map(GithubPrInteractor::commandRecordToCommand) - .collect(toList()); - }); - - for (GithubCommand command : newCommands) { - LOGGER.debug("Marking command {} in pr #{}", command.getComment(), command.getPr()); - - ObjectNode requestBody = new ObjectMapper().createObjectNode() - .put("content", "rocket"); - URI uri = UriBuilder.fromUri("https://api.github.com/") - .path("repos") - .path(ghInfo.getRepoName()) - .path("issues/comments") - .path(Long.toString(command.getComment())) - .path("reactions") - .build(); - HttpRequest request = HttpRequest.newBuilder(uri) - .POST(BodyPublishers.ofString(requestBody.toString())) - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) - .header(HttpHeaders.ACCEPT, "application/vnd.github.squirrel-girl-preview") - .build(); - HttpResponse response = client.send(request, BodyHandlers.ofString()); - - if (response.statusCode() == 200 || response.statusCode() == 201) { - databaseStorage.acquireWriteTransaction(db -> { - db.dsl() - .update(GITHUB_COMMAND) - .set(GITHUB_COMMAND.STATE, GithubCommandState.MARKED_SEEN.getTextualRepresentation()) - .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) - .and(GITHUB_COMMAND.COMMENT.eq(command.getComment())) - .execute(); - }); - } else { - LOGGER.warn("Failed to mark: {}", response.body()); - } - } - } - - private void markCommandsAsQueued(DBWriteAccess db) { - String repoId = repo.getIdAsString(); - db.dsl() - .update(GITHUB_COMMAND) - .set(GITHUB_COMMAND.STATE, GithubCommandState.QUEUED.getTextualRepresentation()) - .where(GITHUB_COMMAND.REPO_ID.eq(repoId)) - .and(GITHUB_COMMAND.COMMIT_HASH.in( - select(TASK.COMMIT_HASH) - .from(TASK) - .where(TASK.REPO_ID.eq(repoId)) - .and(TASK.COMMIT_HASH.isNotNull()) - ).or(GITHUB_COMMAND.COMMIT_HASH.in( - select(RUN.COMMIT_HASH) - .from(RUN) - .where(RUN.REPO_ID.eq(repoId)) - .and(RUN.COMMIT_HASH.isNotNull()) - )) - ) - .execute(); - } - - public void addNewPrCommandsToQueue() { - LOGGER.debug("Adding commits for new commands to queue"); - - // 1. Advance all commands for which a task or run exists to QUEUED - List toBeQueued = databaseStorage.acquireWriteTransaction(db -> { - markCommandsAsQueued(db); - - return db.dsl() - .selectDistinct(GITHUB_COMMAND.COMMIT_HASH) - .from(GITHUB_COMMAND) - .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) - .and(GITHUB_COMMAND.STATE.eq(GithubCommandState.MARKED_SEEN.getTextualRepresentation())) - .stream() - .map(record -> new CommitHash(record.value1())) - .collect(toList()); - }); - - for (CommitHash commitHash : toBeQueued) { - LOGGER.debug("Commit {} will be queued", commitHash.getHash()); - } - - // 2. Try to queue all left-over commands - queue.addCommits("GitHub PR command", repo.getId(), toBeQueued, TaskPriority.USER_CREATED); - - // In theory, new commands might be added between steps 1 and 2 or between steps 2 and 3. In - // practice however, the only way that might happen is through the listener. Since the listener - // ensures that only one update process is running at a time, this should never happen. - // - // Should the code ever be changed such that this is possible, the worst that could happen is - // that new commands' tries are decremented once. That shouldn't really matter since they'll - // still have some tries left over. The tries are meant to absorb various kinds of small - // failures, which is exactly what would happen. - - databaseStorage.acquireWriteTransaction(db -> { - // 3. Same as 1. - markCommandsAsQueued(db); - - // 4. Cancel all left-over commands with only 1 try left - db.dsl() - .update(GITHUB_COMMAND) - .set(GITHUB_COMMAND.STATE, GithubCommandState.ERROR.getTextualRepresentation()) - .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) - .and(GITHUB_COMMAND.STATE.eq(GithubCommandState.MARKED_SEEN.getTextualRepresentation())) - .and(GITHUB_COMMAND.TRIES_LEFT.le(1)) - .execute(); - - // 5. Subtract 1 from the tries of all left-over commands - db.dsl() - .update(GITHUB_COMMAND) - .set(GITHUB_COMMAND.TRIES_LEFT, GITHUB_COMMAND.TRIES_LEFT.minus(1)) - .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) - .and(GITHUB_COMMAND.STATE.eq(GithubCommandState.MARKED_SEEN.getTextualRepresentation())) - .execute(); - }); - } - - private HttpResponse createPrComment(long pr, String body) - throws IOException, InterruptedException { - - ObjectNode requestBody = new ObjectMapper().createObjectNode() - .put("body", body); - URI uri = UriBuilder.fromUri("https://api.github.com/") - .path("repos") - .path(ghInfo.getRepoName()) - .path("issues") - .path(Long.toString(pr)) - .path("comments") - .build(); - HttpRequest request = HttpRequest.newBuilder(uri) - .POST(BodyPublishers.ofString(requestBody.toString())) - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) - .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") - .build(); - return client.send(request, BodyHandlers.ofString()); - } - - public void replyToFinishedPrCommands() throws IOException, InterruptedException { - LOGGER.debug("Replying to finished commands"); - String repoIdStr = repo.getIdAsString(); - - List replies = databaseStorage.acquireReadTransaction(db -> { - return db.dsl() - .selectDistinct(GITHUB_COMMAND.PR, GITHUB_COMMAND.TARGET_BRANCH, GITHUB_COMMAND.COMMIT_HASH, - RUN.ID) - .from(GITHUB_COMMAND) - .join(RUN) - .on(RUN.COMMIT_HASH.eq(GITHUB_COMMAND.COMMIT_HASH)) - .where(GITHUB_COMMAND.REPO_ID.eq(repoIdStr)) - .and(RUN.REPO_ID.eq(repoIdStr)) - .stream() - .map(record -> new FinishedGithubCommand( - record.value1(), - BranchName.fromName(record.value2()), - new CommitHash(record.value3()), - RunId.fromString(record.value4()) - )) - .collect(toList()); - }); - - for (FinishedGithubCommand reply : replies) { - LOGGER - .debug("Replying to PR #{} and hash {}", reply.getPr(), reply.getCommitHash().getHash()); - - // Get run - RunId runId = reply.getRunId(); - Run run = benchmarkAccess.getRun(runId); - - // Get commits and runs to compare to - List compareToHash = commitAccess - .getFirstParentsOfBranch(repo.getId(), reply.getTargetBranch(), reply.getCommitHash()); - // Actually we only care about the first join point - if (compareToHash.size() > 0) { - compareToHash = compareToHash.subList(0, 1); - } - Map runIds = benchmarkAccess.getLatestRunIds(repo.getId(), compareToHash); - List compareToRuns = benchmarkAccess.getRuns(runIds.values()); - - // Determine Significance of run - Set significantDimensions = dimensionAccess.getSignificantDimensions(); - Optional reasons = significanceDetector - .getSignificance(run, compareToRuns, significantDimensions); - Set dimensions = reasons.stream() - .map(SignificanceReasons::getDimensions) - .flatMap(Collection::stream) - .collect(toSet()); - Map infos = dimensionAccess.getDimensionInfoMap(dimensions); - - String message = buildFinishedPrReply(reply, runId, compareToRuns, reasons.orElse(null), - infos); - HttpResponse response = createPrComment(reply.getPr(), message); - - if (response.statusCode() >= 200 && response.statusCode() < 300) { - databaseStorage.acquireWriteTransaction(db -> { - db.dsl() - .deleteFrom(GITHUB_COMMAND) - .where(GITHUB_COMMAND.REPO_ID.eq(repoIdStr)) - .and(GITHUB_COMMAND.PR.eq(reply.getPr())) - .and(GITHUB_COMMAND.COMMIT_HASH.eq(reply.getCommitHash().getHash())) - .execute(); - }); - } else { - LOGGER.warn("Failed to reply: {}", response.body()); - } - } - } - - private String buildFinishedPrReply(FinishedGithubCommand reply, RunId runId, - List compareToRuns, @Nullable SignificanceReasons reasons, - Map infos) { - - StringBuilder builder = new StringBuilder(); - - builder - .append("Here are the [benchmark results](") - .append(frontendUrl) - .append("run-detail/") - .append(runId.getIdAsString()) - .append(") for commit ") - .append(reply.getCommitHash().getHash()) - .append("."); - - if (reasons == null) { - if (compareToRuns.isEmpty()) { - builder.append("Found no runs to compare against."); - } else if (compareToRuns.size() == 1) { - Run run = compareToRuns.get(0); - builder - .append("\nThere were [no significant changes](") - .append(frontendUrl) - .append("compare/") - .append(run.getId().getIdAsString()) - .append("/to/") - .append(runId.getIdAsString()) - .append(") against commit ") - .append(run.getSource().getLeft().get().getHash().getHash()) - .append("."); - } else { - builder.append("\nThere were no significant changes."); - } - return builder.toString(); - } - - if (reasons.isEntireRunFailed()) { - builder.append("\nThe entire run failed."); - } - - if (!reasons.getSignificantFailedDimensions().isEmpty()) { - builder.append("\nThese dimensions failed:"); - buildSignificanceDiff(builder, List.of(), reasons.getSignificantFailedDimensions(), infos); - } - - if (compareToRuns.isEmpty()) { - builder.append("\nFound no runs to compare against."); - } else if (reasons.getSignificantDifferences().isEmpty()) { - builder.append("\nFound no significant differences."); - } else { - Map> diffsByRun = reasons.getSignificantDifferences() - .stream() - .collect(groupingBy(DimensionDifference::getOldRunId)); - for (Run run : compareToRuns) { - List diffs = diffsByRun.get(run.getId()); - if (diffs == null || diffs.isEmpty()) { - continue; - } - if (run.getSource().getLeft().isEmpty()) { - // This should never happen since all runs are obtained from commits - continue; - } - - builder - .append("\nThere were [significant changes](") - .append(frontendUrl) - .append("compare/") - .append(run.getId().getIdAsString()) - .append("/to/") - .append(runId.getIdAsString()) - .append(") against commit ") - .append(run.getSource().getLeft().get().getHash().getHash()) - .append(":"); - buildSignificanceDiff(builder, diffs, List.of(), infos); - } - } - - return builder.toString(); - } - - private static class TableLine { - - // Left-aligned - public final String prefix; - public final String benchmark; - public final String metric; - // Right-aligned - public final String percentage; - @Nullable - public final String stddev; - - public TableLine(String prefix, String benchmark, String metric, String percentage, - @Nullable String stddev) { - this.prefix = prefix; - this.benchmark = benchmark; - this.metric = metric; - this.percentage = percentage; - this.stddev = stddev; - } - - public Optional getStddev() { - return Optional.ofNullable(stddev); - } - } - - private static String ljust(int width, String string) { - return String.format("%-" + width + "s", string); - } - - private static String rjust(int width, String string) { - return String.format("%" + width + "s", string); - } - - public static void buildSignificanceDiff(StringBuilder builder, - List differences, List failed, - Map infos) { - - List lines = new ArrayList<>(); - differences.stream() - .map(diff -> { - DimensionInfo info = infos.get(diff.getDimension()); - boolean less_is_better = info.getInterpretation() == Interpretation.LESS_IS_BETTER; - boolean more_is_better = info.getInterpretation() == Interpretation.MORE_IS_BETTER; - boolean less = diff.getDiff() < 0; - boolean more = diff.getDiff() > 0; - final String prefix; - if ((less_is_better && less) || (more_is_better && more)) { - prefix = "+"; - } else if ((less_is_better && more) || (more_is_better && less)) { - prefix = "-"; - } else { - prefix = " "; - } - - return new TableLine( - prefix, - diff.getDimension().getBenchmark(), - diff.getDimension().getMetric(), - diff.getReldiff().map(d -> String.format(Locale.ROOT, "%.1f%%", d * 100)).orElse("-"), - diff.getStddevDiff().map(d -> String.format(Locale.ROOT, "(%.1f σ)", d)).orElse(null) - ); - }) - .forEach(lines::add); - failed.stream() - .map(dim -> new TableLine("-", dim.getBenchmark(), dim.getMetric(), "failed", null)) - .forEach(lines::add); - lines.sort(comparing(line -> new Dimension(line.benchmark, line.metric))); - - TableLine legend = new TableLine(" ", "Benchmark", "Metric", "Change", null); - int maxBenchWidth = Stream.concat(Stream.of(legend), lines.stream()) - .mapToInt(line -> line.benchmark.length()) - .max() - .orElse(0); - int maxMetricWidth = Stream.concat(Stream.of(legend), lines.stream()) - .mapToInt(line -> line.metric.length()) - .max() - .orElse(0); - int maxChangeWidth = Stream.concat(Stream.of(legend), lines.stream()) - .mapToInt(line -> line.percentage.length()) - .max() - .orElse(0); - int maxStddevWidth = Stream.concat(Stream.of(legend), lines.stream()) - .flatMap(line -> line.getStddev().stream()) - .mapToInt(String::length) - .max() - .orElse(0); - int totalWidthWithoutPrefix = maxBenchWidth + 3 + maxMetricWidth + 3 + maxChangeWidth; - if (maxStddevWidth > 0) { - totalWidthWithoutPrefix += 1 + maxStddevWidth; - } - - builder.append("\n```diff"); - - // Legend - builder - .append("\n") - .append(legend.prefix) - .append(" ") - .append(ljust(maxBenchWidth, legend.benchmark)) - .append(" ") - .append(ljust(maxMetricWidth, legend.metric)) - .append(" ") - .append(rjust(maxChangeWidth, legend.percentage)); - // Separator - builder - .append("\n ") - .append("=".repeat(totalWidthWithoutPrefix)); - // Actual lines - for (TableLine line : lines) { - builder - .append("\n") - .append(line.prefix) - .append(" ") - .append(ljust(maxBenchWidth, line.benchmark)) - .append(" ") - .append(ljust(maxMetricWidth, line.metric)) - .append(" ") - .append(rjust(maxChangeWidth, line.percentage)); - - if (line.stddev != null) { - builder - .append(" ") - .append(rjust(maxStddevWidth, line.stddev)); - } - } - - builder.append("\n```"); - } - - public void replyToErroredPrCommands() throws IOException, InterruptedException { - LOGGER.debug("Replying to errored commands"); - String repoId = repo.getIdAsString(); - - List> replies = databaseStorage.acquireReadTransaction(db -> { - return db.dsl() - .selectDistinct(GITHUB_COMMAND.PR, GITHUB_COMMAND.COMMIT_HASH) - .from(GITHUB_COMMAND) - .where(GITHUB_COMMAND.STATE.eq(GithubCommandState.ERROR.getTextualRepresentation())) - .stream() - .map(record -> new Pair<>( - record.value1(), - new CommitHash(record.value2()) - )) - .collect(toList()); - }); - - for (Pair reply : replies) { - LOGGER.debug("Replying to PR #{} and hash {}", reply.getFirst(), reply.getSecond().getHash()); - - String body = "Failed to benchmark commit " - + reply.getSecond().getHash() - + " after multiple tries."; - HttpResponse response = createPrComment(reply.getFirst(), body); - - if (response.statusCode() >= 200 && response.statusCode() < 300) { - databaseStorage.acquireWriteTransaction(db -> { - db.dsl() - .deleteFrom(GITHUB_COMMAND) - .where(GITHUB_COMMAND.REPO_ID.eq(repoId)) - .and(GITHUB_COMMAND.PR.eq(reply.getFirst())) - .and(GITHUB_COMMAND.COMMIT_HASH.eq(reply.getSecond().getHash())) - .execute(); - }); - } else { - LOGGER.warn("Failed to reply: {}", response.body()); - } - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(GithubPrInteractor.class); + + private final Repo repo; + private final GithubInfo ghInfo; + private final DatabaseStorage databaseStorage; + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final SignificanceDetector significanceDetector; + private final Queue queue; + + private final String frontendUrl; + + private final HttpClient client; + private final JsonBodyHandler jsonBodyHandler; + private final String basicAuthHeader; + + private GithubPrInteractor( + Repo repo, + GithubInfo ghInfo, + DatabaseStorage databaseStorage, + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + SignificanceDetector significanceDetector, + Queue queue, + String frontendUrl) { + + this.repo = repo; + this.ghInfo = ghInfo; + this.databaseStorage = databaseStorage; + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.significanceDetector = significanceDetector; + this.queue = queue; + + this.frontendUrl = frontendUrl; + + client = HttpClient.newHttpClient(); + jsonBodyHandler = new JsonBodyHandler(); + + String authInfo = ":" + ghInfo.getAccessToken(); + byte[] authInfoBytes = authInfo.getBytes(StandardCharsets.UTF_8); + String authInfoBase64 = Base64.getEncoder().encodeToString(authInfoBytes); + basicAuthHeader = "Basic " + authInfoBase64; + } + + public static Optional fromRepo( + Repo repo, + DatabaseStorage databaseStorage, + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + SignificanceDetector significanceDetector, + Queue queue, + String frontendUrl) { + + Optional ghInfoOpt = repo.getGithubInfo(); + return ghInfoOpt.map( + githubInfo -> + new GithubPrInteractor( + repo, + githubInfo, + databaseStorage, + benchmarkAccess, + commitAccess, + dimensionAccess, + significanceDetector, + queue, + frontendUrl)); + } + + private static GithubCommand commandRecordToCommand(GithubCommandRecord record) { + return new GithubCommand( + RepoId.fromString(record.getRepoId()), + record.getPr(), + BranchName.fromName(record.getTargetBranch()), + record.getComment(), + new CommitHash(record.getCommitHash()), + GithubCommandState.fromTextualRepresentation(record.getState()), + record.getTriesLeft()); + } + + private void checkResponse(HttpResponse response) throws GithubApiError { + if (response.statusCode() < 100 || response.statusCode() >= 300) { + JsonNode message = response.body().get("message"); + if (message == null) { + throw new GithubApiError("Something went wrong", response.uri()); + } else { + throw new GithubApiError(message.toString(), response.uri()); + } + } + } + + private JsonNode getResourceFromUrl(String issueUrl) + throws URISyntaxException, IOException, InterruptedException, GithubApiError { + + URI uri = new URI(issueUrl); + HttpRequest request = + HttpRequest.newBuilder(uri) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) + .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") + .build(); + HttpResponse response = client.send(request, jsonBodyHandler); + checkResponse(response); + return response.body(); + } + + private JsonNode getCommentPage(int page) + throws IOException, InterruptedException, GithubApiError { + + URI uri = + UriBuilder.fromUri("https://api.github.com/") + .path("repos") + .path(ghInfo.getRepoName()) + .path("issues/comments") + .queryParam("since", ghInfo.getCommentCutoff().toString()) + .queryParam("sort", "created") + // The direction is important! If we used "asc" instead, we might lose older comments if + // comments are updated in-between fetching different pages. With "desc", we instead see + // some + // comments multiple times in the same scenario (and we'll be able to get the comments + // we + // missed on the next run). + .queryParam("direction", "desc") + .queryParam("page", page) + .build(); + HttpRequest request = + HttpRequest.newBuilder(uri) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) + .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") + .build(); + HttpResponse response = client.send(request, jsonBodyHandler); + checkResponse(response); + return response.body(); + } + + private Optional findNewCommandCandidates( + List> commandCandidates) + throws IOException, InterruptedException, URISyntaxException, GithubApiError { + + Set seen = new HashSet<>(); + Instant commentCutoff = ghInfo.getCommentCutoff(); + Optional newCommentCutoff = Optional.empty(); + + for (int page = 1; true; page++) { + LOGGER.debug("Looking at comment page {}", page); + JsonNode comments = getCommentPage(page); + if (comments.isEmpty()) { + LOGGER.debug("Page {} is empty", page); + break; // We've hit the last page + } + + for (JsonNode comment : comments) { + Instant createdAt = Instant.parse(comment.get("created_at").asText()); + if (createdAt.isBefore(commentCutoff) || createdAt.equals(commentCutoff)) { + // The "since" query parameter only cuts off comments that were updated before the + // specified cutoff. However, we don't want to look at any comments *created* before the + // cutoff, so we need to do a bit of extra filtering. + LOGGER.debug("Ignoring comment created before comment cutoff"); + continue; + } + + if (newCommentCutoff.isEmpty()) { + // We know that this comment is the first comment returned by the GitHub API. Since the + // comments are sorted by creation time in descending order, we know that this is the + // newest comment. + newCommentCutoff = Optional.of(createdAt); + } + + long commentId = comment.get("id").asLong(); + if (seen.contains(commentId)) { + // New comments may have appeared since we requested the previous page, leading to + // comments we've already seen being pushed to this (next) page. + LOGGER.debug("Ignoring comment we've already seen"); + continue; + } + seen.add(commentId); + + String commentBody = comment.get("body").asText(); + if (!commentBody.strip().startsWith("!bench")) { + LOGGER.debug("Ignoring comment without command ({})", commentBody); + continue; // Not a command, just a normal comment + } + + JsonNode issue = getResourceFromUrl(comment.get("issue_url").asText()); + if (!issue.has("pull_request")) { + LOGGER.debug("Ignoring issue comment ({})", commentBody); + continue; // Not a PR, just a normal issue + } + + JsonNode pr = getResourceFromUrl(issue.get("pull_request").get("url").asText()); + + long prNumber = pr.get("number").asLong(); + BranchName targetBranch = BranchName.fromName(pr.get("base").get("ref").asText()); + CommitHash commitHash = new CommitHash(pr.get("head").get("sha").asText()); + GithubCommand command = + new GithubCommand(repo.getId(), prNumber, targetBranch, commentId, commitHash); + String username = comment.get("user").get("login").asText(); + commandCandidates.add(new Pair<>(command, username)); + LOGGER.debug("Found command for pr #{} ({}, by {})", prNumber, commentBody, username); + } + } + + return newCommentCutoff; + } + + private List keepOnlyAuthorizedCandidates( + List> commandCandidates) + throws IOException, InterruptedException, GithubApiError { + + Set usernames = commandCandidates.stream().map(Pair::getSecond).collect(toSet()); + + Set usersWithWritePermission = new HashSet<>(); + for (String username : usernames) { + // HACK: Temporarily disable permission checks + usersWithWritePermission.add(username); + + LOGGER.debug("Checking if {} has write permissions", username); + URI url = + UriBuilder.fromUri("https://api.github.com/") + .path("repos") + .path(ghInfo.getRepoName()) + .path("collaborators") + .path(username) + .path("permission") + .build(); + HttpRequest request = + HttpRequest.newBuilder(url) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) + .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") + .build(); + HttpResponse response = client.send(request, jsonBodyHandler); + checkResponse(response); + if (response.statusCode() != 200) { + LOGGER.debug("User {} doesn't have write permissions (not a collaborator)", username); + continue; + } + String permission = response.body().get("permission").asText(); + if (permission.equals("admin") || permission.equals("write")) { + LOGGER.debug("User {} has write permissions ({})", username, permission); + usersWithWritePermission.add(username); + } else { + LOGGER.debug("User {} doesn't have write permissions ({})", username, permission); + } + } + + return commandCandidates.stream() + .filter(pair -> usersWithWritePermission.contains(pair.getSecond())) + .map(Pair::getFirst) + .collect(toList()); + } + + public void searchForNewPrCommands() + throws IOException, InterruptedException, URISyntaxException, GithubApiError { + + LOGGER.debug("Searching for new PR commands"); + + List> commandCandidates = new ArrayList<>(); + Optional newCommentCutoff = findNewCommandCandidates(commandCandidates); + List commands = keepOnlyAuthorizedCandidates(commandCandidates); + + if (newCommentCutoff.isEmpty() || commands.isEmpty()) { + // If we don't have a new cutoff, definitely haven't found any new commands either + return; + } + Instant newCommentCutoffValue = newCommentCutoff.get(); + + databaseStorage.acquireWriteTransaction( + db -> { + RepoRecord repoRecord = + db.dsl().selectFrom(REPO).where(REPO.ID.eq(repo.getIdAsString())).fetchSingle(); + if (repoRecord.getGithubCommentCutoff() == null) { + return; // While we were crawling, GitHub integration has been turned off + } + repoRecord.setGithubCommentCutoff(newCommentCutoffValue); + db.dsl().batchUpdate(repoRecord).execute(); + + List commandRecords = + commands.stream() + .map( + command -> + new GithubCommandRecord( + command.getRepoId().getIdAsString(), + command.getPr(), + command.getTargetBranch().getName(), + command.getComment(), + command.getCommitHash().getHash(), + command.getState().getTextualRepresentation(), + command.getTriesLeft())) + .collect(toList()); + db.dsl().batchInsert(commandRecords).execute(); + }); + } + + public void markNewPrCommandsAsSeen() throws IOException, InterruptedException { + LOGGER.debug("Marking new commands as seen"); + + List newCommands = + databaseStorage.acquireReadTransaction( + db -> { + return db + .dsl() + .selectFrom(GITHUB_COMMAND) + .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) + .and(GITHUB_COMMAND.STATE.eq(GithubCommandState.NEW.getTextualRepresentation())) + .stream() + .map(GithubPrInteractor::commandRecordToCommand) + .collect(toList()); + }); + + for (GithubCommand command : newCommands) { + LOGGER.debug("Marking command {} in pr #{}", command.getComment(), command.getPr()); + + ObjectNode requestBody = new ObjectMapper().createObjectNode().put("content", "rocket"); + URI uri = + UriBuilder.fromUri("https://api.github.com/") + .path("repos") + .path(ghInfo.getRepoName()) + .path("issues/comments") + .path(Long.toString(command.getComment())) + .path("reactions") + .build(); + HttpRequest request = + HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(requestBody.toString())) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) + .header(HttpHeaders.ACCEPT, "application/vnd.github.squirrel-girl-preview") + .build(); + HttpResponse response = client.send(request, BodyHandlers.ofString()); + + if (response.statusCode() == 200 || response.statusCode() == 201) { + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .update(GITHUB_COMMAND) + .set( + GITHUB_COMMAND.STATE, + GithubCommandState.MARKED_SEEN.getTextualRepresentation()) + .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) + .and(GITHUB_COMMAND.COMMENT.eq(command.getComment())) + .execute(); + }); + } else { + LOGGER.warn("Failed to mark: {}", response.body()); + } + } + } + + private void markCommandsAsQueued(DBWriteAccess db) { + String repoId = repo.getIdAsString(); + db.dsl() + .update(GITHUB_COMMAND) + .set(GITHUB_COMMAND.STATE, GithubCommandState.QUEUED.getTextualRepresentation()) + .where(GITHUB_COMMAND.REPO_ID.eq(repoId)) + .and( + GITHUB_COMMAND + .COMMIT_HASH + .in( + select(TASK.COMMIT_HASH) + .from(TASK) + .where(TASK.REPO_ID.eq(repoId)) + .and(TASK.COMMIT_HASH.isNotNull())) + .or( + GITHUB_COMMAND.COMMIT_HASH.in( + select(RUN.COMMIT_HASH) + .from(RUN) + .where(RUN.REPO_ID.eq(repoId)) + .and(RUN.COMMIT_HASH.isNotNull())))) + .execute(); + } + + public void addNewPrCommandsToQueue() { + LOGGER.debug("Adding commits for new commands to queue"); + + // 1. Advance all commands for which a task or run exists to QUEUED + List toBeQueued = + databaseStorage.acquireWriteTransaction( + db -> { + markCommandsAsQueued(db); + + return db + .dsl() + .selectDistinct(GITHUB_COMMAND.COMMIT_HASH) + .from(GITHUB_COMMAND) + .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) + .and( + GITHUB_COMMAND.STATE.eq( + GithubCommandState.MARKED_SEEN.getTextualRepresentation())) + .stream() + .map(record -> new CommitHash(record.value1())) + .collect(toList()); + }); + + for (CommitHash commitHash : toBeQueued) { + LOGGER.debug("Commit {} will be queued", commitHash.getHash()); + } + + // 2. Try to queue all left-over commands + queue.addCommits("GitHub PR command", repo.getId(), toBeQueued, TaskPriority.USER_CREATED); + + // In theory, new commands might be added between steps 1 and 2 or between steps 2 and 3. In + // practice however, the only way that might happen is through the listener. Since the listener + // ensures that only one update process is running at a time, this should never happen. + // + // Should the code ever be changed such that this is possible, the worst that could happen is + // that new commands' tries are decremented once. That shouldn't really matter since they'll + // still have some tries left over. The tries are meant to absorb various kinds of small + // failures, which is exactly what would happen. + + databaseStorage.acquireWriteTransaction( + db -> { + // 3. Same as 1. + markCommandsAsQueued(db); + + // 4. Cancel all left-over commands with only 1 try left + db.dsl() + .update(GITHUB_COMMAND) + .set(GITHUB_COMMAND.STATE, GithubCommandState.ERROR.getTextualRepresentation()) + .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) + .and( + GITHUB_COMMAND.STATE.eq( + GithubCommandState.MARKED_SEEN.getTextualRepresentation())) + .and(GITHUB_COMMAND.TRIES_LEFT.le(1)) + .execute(); + + // 5. Subtract 1 from the tries of all left-over commands + db.dsl() + .update(GITHUB_COMMAND) + .set(GITHUB_COMMAND.TRIES_LEFT, GITHUB_COMMAND.TRIES_LEFT.minus(1)) + .where(GITHUB_COMMAND.REPO_ID.eq(repo.getIdAsString())) + .and( + GITHUB_COMMAND.STATE.eq( + GithubCommandState.MARKED_SEEN.getTextualRepresentation())) + .execute(); + }); + } + + private HttpResponse createPrComment(long pr, String body) + throws IOException, InterruptedException { + + ObjectNode requestBody = new ObjectMapper().createObjectNode().put("body", body); + URI uri = + UriBuilder.fromUri("https://api.github.com/") + .path("repos") + .path(ghInfo.getRepoName()) + .path("issues") + .path(Long.toString(pr)) + .path("comments") + .build(); + HttpRequest request = + HttpRequest.newBuilder(uri) + .POST(BodyPublishers.ofString(requestBody.toString())) + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader) + .header(HttpHeaders.ACCEPT, "application/vnd.github.v3+json") + .build(); + return client.send(request, BodyHandlers.ofString()); + } + + public void replyToFinishedPrCommands() throws IOException, InterruptedException { + LOGGER.debug("Replying to finished commands"); + String repoIdStr = repo.getIdAsString(); + + List replies = + databaseStorage.acquireReadTransaction( + db -> { + return db + .dsl() + .selectDistinct( + GITHUB_COMMAND.PR, + GITHUB_COMMAND.TARGET_BRANCH, + GITHUB_COMMAND.COMMIT_HASH, + RUN.ID) + .from(GITHUB_COMMAND) + .join(RUN) + .on(RUN.COMMIT_HASH.eq(GITHUB_COMMAND.COMMIT_HASH)) + .where(GITHUB_COMMAND.REPO_ID.eq(repoIdStr)) + .and(RUN.REPO_ID.eq(repoIdStr)) + .stream() + .map( + record -> + new FinishedGithubCommand( + record.value1(), + BranchName.fromName(record.value2()), + new CommitHash(record.value3()), + RunId.fromString(record.value4()))) + .collect(toList()); + }); + + for (FinishedGithubCommand reply : replies) { + LOGGER.debug( + "Replying to PR #{} and hash {}", reply.getPr(), reply.getCommitHash().getHash()); + + // Get run + RunId runId = reply.getRunId(); + Run run = benchmarkAccess.getRun(runId); + + // Get commits and runs to compare to + List compareToHash = + commitAccess.getFirstParentsOfBranch( + repo.getId(), reply.getTargetBranch(), reply.getCommitHash()); + // Actually we only care about the first join point + if (compareToHash.size() > 0) { + compareToHash = compareToHash.subList(0, 1); + } + Map runIds = benchmarkAccess.getLatestRunIds(repo.getId(), compareToHash); + List compareToRuns = benchmarkAccess.getRuns(runIds.values()); + + // Determine Significance of run + Set significantDimensions = dimensionAccess.getSignificantDimensions(); + Optional reasons = + significanceDetector.getSignificance(run, compareToRuns, significantDimensions); + Set dimensions = + reasons.stream() + .map(SignificanceReasons::getDimensions) + .flatMap(Collection::stream) + .collect(toSet()); + Map infos = dimensionAccess.getDimensionInfoMap(dimensions); + + String message = + buildFinishedPrReply(reply, runId, compareToRuns, reasons.orElse(null), infos); + HttpResponse response = createPrComment(reply.getPr(), message); + + if (response.statusCode() >= 200 && response.statusCode() < 300) { + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .deleteFrom(GITHUB_COMMAND) + .where(GITHUB_COMMAND.REPO_ID.eq(repoIdStr)) + .and(GITHUB_COMMAND.PR.eq(reply.getPr())) + .and(GITHUB_COMMAND.COMMIT_HASH.eq(reply.getCommitHash().getHash())) + .execute(); + }); + } else { + LOGGER.warn("Failed to reply: {}", response.body()); + } + } + } + + private String buildFinishedPrReply( + FinishedGithubCommand reply, + RunId runId, + List compareToRuns, + @Nullable SignificanceReasons reasons, + Map infos) { + + StringBuilder builder = new StringBuilder(); + + builder + .append("Here are the [benchmark results](") + .append(frontendUrl) + .append("run-detail/") + .append(runId.getIdAsString()) + .append(") for commit ") + .append(reply.getCommitHash().getHash()) + .append("."); + + if (reasons == null) { + if (compareToRuns.isEmpty()) { + builder.append("Found no runs to compare against."); + } else if (compareToRuns.size() == 1) { + Run run = compareToRuns.get(0); + builder + .append("\nThere were [no significant changes](") + .append(frontendUrl) + .append("compare/") + .append(run.getId().getIdAsString()) + .append("/to/") + .append(runId.getIdAsString()) + .append(") against commit ") + .append(run.getSource().getLeft().get().getHash().getHash()) + .append("."); + } else { + builder.append("\nThere were no significant changes."); + } + return builder.toString(); + } + + if (reasons.isEntireRunFailed()) { + builder.append("\nThe entire run failed."); + } + + if (!reasons.getSignificantFailedDimensions().isEmpty()) { + builder.append("\nThese dimensions failed:"); + buildSignificanceDiff(builder, List.of(), reasons.getSignificantFailedDimensions(), infos); + } + + if (compareToRuns.isEmpty()) { + builder.append("\nFound no runs to compare against."); + } else if (reasons.getSignificantDifferences().isEmpty()) { + builder.append("\nFound no significant differences."); + } else { + Map> diffsByRun = + reasons.getSignificantDifferences().stream() + .collect(groupingBy(DimensionDifference::getOldRunId)); + for (Run run : compareToRuns) { + List diffs = diffsByRun.get(run.getId()); + if (diffs == null || diffs.isEmpty()) { + continue; + } + if (run.getSource().getLeft().isEmpty()) { + // This should never happen since all runs are obtained from commits + continue; + } + + builder + .append("\nThere were [significant changes](") + .append(frontendUrl) + .append("compare/") + .append(run.getId().getIdAsString()) + .append("/to/") + .append(runId.getIdAsString()) + .append(") against commit ") + .append(run.getSource().getLeft().get().getHash().getHash()) + .append(":"); + buildSignificanceDiff(builder, diffs, List.of(), infos); + } + } + + return builder.toString(); + } + + private static class TableLine { + + // Left-aligned + public final String prefix; + public final String benchmark; + public final String metric; + // Right-aligned + public final String percentage; + @Nullable public final String stddev; + + public TableLine( + String prefix, + String benchmark, + String metric, + String percentage, + @Nullable String stddev) { + this.prefix = prefix; + this.benchmark = benchmark; + this.metric = metric; + this.percentage = percentage; + this.stddev = stddev; + } + + public Optional getStddev() { + return Optional.ofNullable(stddev); + } + } + + private static String ljust(int width, String string) { + return String.format("%-" + width + "s", string); + } + + private static String rjust(int width, String string) { + return String.format("%" + width + "s", string); + } + + public static void buildSignificanceDiff( + StringBuilder builder, + List differences, + List failed, + Map infos) { + + List lines = new ArrayList<>(); + differences.stream() + .map( + diff -> { + DimensionInfo info = infos.get(diff.getDimension()); + boolean less_is_better = info.getInterpretation() == Interpretation.LESS_IS_BETTER; + boolean more_is_better = info.getInterpretation() == Interpretation.MORE_IS_BETTER; + boolean less = diff.getDiff() < 0; + boolean more = diff.getDiff() > 0; + final String prefix; + if ((less_is_better && less) || (more_is_better && more)) { + prefix = "+"; + } else if ((less_is_better && more) || (more_is_better && less)) { + prefix = "-"; + } else { + prefix = " "; + } + + return new TableLine( + prefix, + diff.getDimension().getBenchmark(), + diff.getDimension().getMetric(), + diff.getReldiff() + .map(d -> String.format(Locale.ROOT, "%.1f%%", d * 100)) + .orElse("-"), + diff.getStddevDiff() + .map(d -> String.format(Locale.ROOT, "(%.1f σ)", d)) + .orElse(null)); + }) + .forEach(lines::add); + failed.stream() + .map(dim -> new TableLine("-", dim.getBenchmark(), dim.getMetric(), "failed", null)) + .forEach(lines::add); + lines.sort(comparing(line -> new Dimension(line.benchmark, line.metric))); + + TableLine legend = new TableLine(" ", "Benchmark", "Metric", "Change", null); + int maxBenchWidth = + Stream.concat(Stream.of(legend), lines.stream()) + .mapToInt(line -> line.benchmark.length()) + .max() + .orElse(0); + int maxMetricWidth = + Stream.concat(Stream.of(legend), lines.stream()) + .mapToInt(line -> line.metric.length()) + .max() + .orElse(0); + int maxChangeWidth = + Stream.concat(Stream.of(legend), lines.stream()) + .mapToInt(line -> line.percentage.length()) + .max() + .orElse(0); + int maxStddevWidth = + Stream.concat(Stream.of(legend), lines.stream()) + .flatMap(line -> line.getStddev().stream()) + .mapToInt(String::length) + .max() + .orElse(0); + int totalWidthWithoutPrefix = maxBenchWidth + 3 + maxMetricWidth + 3 + maxChangeWidth; + if (maxStddevWidth > 0) { + totalWidthWithoutPrefix += 1 + maxStddevWidth; + } + + builder.append("\n```diff"); + + // Legend + builder + .append("\n") + .append(legend.prefix) + .append(" ") + .append(ljust(maxBenchWidth, legend.benchmark)) + .append(" ") + .append(ljust(maxMetricWidth, legend.metric)) + .append(" ") + .append(rjust(maxChangeWidth, legend.percentage)); + // Separator + builder.append("\n ").append("=".repeat(totalWidthWithoutPrefix)); + // Actual lines + for (TableLine line : lines) { + builder + .append("\n") + .append(line.prefix) + .append(" ") + .append(ljust(maxBenchWidth, line.benchmark)) + .append(" ") + .append(ljust(maxMetricWidth, line.metric)) + .append(" ") + .append(rjust(maxChangeWidth, line.percentage)); + + if (line.stddev != null) { + builder.append(" ").append(rjust(maxStddevWidth, line.stddev)); + } + } + + builder.append("\n```"); + } + + public void replyToErroredPrCommands() throws IOException, InterruptedException { + LOGGER.debug("Replying to errored commands"); + String repoId = repo.getIdAsString(); + + List> replies = + databaseStorage.acquireReadTransaction( + db -> { + return db + .dsl() + .selectDistinct(GITHUB_COMMAND.PR, GITHUB_COMMAND.COMMIT_HASH) + .from(GITHUB_COMMAND) + .where( + GITHUB_COMMAND.STATE.eq(GithubCommandState.ERROR.getTextualRepresentation())) + .stream() + .map(record -> new Pair<>(record.value1(), new CommitHash(record.value2()))) + .collect(toList()); + }); + + for (Pair reply : replies) { + LOGGER.debug("Replying to PR #{} and hash {}", reply.getFirst(), reply.getSecond().getHash()); + + String body = + "Failed to benchmark commit " + reply.getSecond().getHash() + " after multiple tries."; + HttpResponse response = createPrComment(reply.getFirst(), body); + + if (response.statusCode() >= 200 && response.statusCode() < 300) { + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .deleteFrom(GITHUB_COMMAND) + .where(GITHUB_COMMAND.REPO_ID.eq(repoId)) + .and(GITHUB_COMMAND.PR.eq(reply.getFirst())) + .and(GITHUB_COMMAND.COMMIT_HASH.eq(reply.getSecond().getHash())) + .execute(); + }); + } else { + LOGGER.warn("Failed to reply: {}", response.body()); + } + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/JsonBodyHandler.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/JsonBodyHandler.java index 156ff66a6..6698a1ef2 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/JsonBodyHandler.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/github/JsonBodyHandler.java @@ -12,17 +12,16 @@ public class JsonBodyHandler implements BodyHandler { - @Override - public BodySubscriber apply(ResponseInfo responseInfo) { - return HttpResponse.BodySubscribers.mapping( - HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), - s -> { - try { - return Jackson.newObjectMapper().readTree(s); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - ); - } + @Override + public BodySubscriber apply(ResponseInfo responseInfo) { + return HttpResponse.BodySubscribers.mapping( + HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), + s -> { + try { + return Jackson.newObjectMapper().readTree(s); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommit.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommit.java index ea3abcf15..88df8e90d 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommit.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommit.java @@ -4,60 +4,64 @@ import java.time.Instant; import java.util.Set; -/** - * Wrapper class for commits from a jgit repository. - */ +/** Wrapper class for commits from a jgit repository. */ public class JgitCommit { - private final CommitHash hash; - private final Set parentHashes; - private final String author; - private final Instant authorDate; - private final String committer; - private final Instant committerDate; - private final String message; - - public JgitCommit(CommitHash hash, Set parentHashes, String author, - Instant authorDate, String committer, Instant committerDate, String message) { - - this.hash = hash; - this.parentHashes = parentHashes; - this.author = author; - this.authorDate = authorDate; - this.committer = committer; - this.committerDate = committerDate; - this.message = message; - } - - public CommitHash getHash() { - return hash; - } - - public String getHashAsString() { - return hash.getHash(); - } - - public Set getParentHashes() { - return parentHashes; - } - - public String getAuthor() { - return author; - } - - public Instant getAuthorDate() { - return authorDate; - } - - public String getCommitter() { - return committer; - } - - public Instant getCommitterDate() { - return committerDate; - } - - public String getMessage() { - return message; - } + private final CommitHash hash; + private final Set parentHashes; + private final String author; + private final Instant authorDate; + private final String committer; + private final Instant committerDate; + private final String message; + + public JgitCommit( + CommitHash hash, + Set parentHashes, + String author, + Instant authorDate, + String committer, + Instant committerDate, + String message) { + + this.hash = hash; + this.parentHashes = parentHashes; + this.author = author; + this.authorDate = authorDate; + this.committer = committer; + this.committerDate = committerDate; + this.message = message; + } + + public CommitHash getHash() { + return hash; + } + + public String getHashAsString() { + return hash.getHash(); + } + + public Set getParentHashes() { + return parentHashes; + } + + public String getAuthor() { + return author; + } + + public Instant getAuthorDate() { + return authorDate; + } + + public String getCommitter() { + return committer; + } + + public Instant getCommitterDate() { + return committerDate; + } + + public String getMessage() { + return message; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommitWalk.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommitWalk.java index 7f1ef2aad..b4c8a7135 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommitWalk.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/listener/jgitutils/JgitCommitWalk.java @@ -1,6 +1,5 @@ package de.aaaaaaah.velcom.backend.listener.jgitutils; - import static java.util.stream.Collectors.toSet; import de.aaaaaaah.velcom.backend.access.committaccess.entities.CommitHash; @@ -25,66 +24,65 @@ */ public class JgitCommitWalk implements AutoCloseable { - private final Repository repo; - private final RevWalk walk; - - public JgitCommitWalk(Repository repo) { - this.repo = repo; - this.walk = new RevWalk(repo); - } - - private static String formatPersonIdent(PersonIdent ident) { - final String name = ident.getName(); - - if (name.isEmpty()) { - return "<" + ident.getEmailAddress() + ">"; - } else { - return name + " <" + ident.getEmailAddress() + ">"; - } - } - - private static JgitCommit revCommitToJgitCommit(RevCommit revCommit) { - CommitHash hash = new CommitHash(revCommit.getId().getName()); - - Set parentHashes = List.of(revCommit.getParents()).stream() - .map(RevCommit::getId) - .map(AnyObjectId::getName) - .map(CommitHash::new) - .collect(toSet()); - - PersonIdent authorIdent = revCommit.getAuthorIdent(); - PersonIdent committerIdent = revCommit.getCommitterIdent(); - - return new JgitCommit( - hash, - parentHashes, - formatPersonIdent(authorIdent), - authorIdent.getWhen().toInstant(), - formatPersonIdent(committerIdent), - committerIdent.getWhen().toInstant(), - revCommit.getFullMessage() - ); - } - - public Optional getCommit(CommitHash hash) { - try { - ObjectId commitPtr = repo.resolve(hash.getHash()); - RevCommit revCommit = walk.parseCommit(commitPtr); - return Optional.of(revCommitToJgitCommit(revCommit)); - } catch (IOException e) { - return Optional.empty(); - } - } - - public Stream getAllCommits() throws IOException, GitAPIException { - Iterable logIterable = new Git(repo).log().all().call(); - return StreamSupport.stream(logIterable.spliterator(), false) - .map(JgitCommitWalk::revCommitToJgitCommit); - } - - @Override - public void close() { - walk.close(); - } - + private final Repository repo; + private final RevWalk walk; + + public JgitCommitWalk(Repository repo) { + this.repo = repo; + this.walk = new RevWalk(repo); + } + + private static String formatPersonIdent(PersonIdent ident) { + final String name = ident.getName(); + + if (name.isEmpty()) { + return "<" + ident.getEmailAddress() + ">"; + } else { + return name + " <" + ident.getEmailAddress() + ">"; + } + } + + private static JgitCommit revCommitToJgitCommit(RevCommit revCommit) { + CommitHash hash = new CommitHash(revCommit.getId().getName()); + + Set parentHashes = + List.of(revCommit.getParents()).stream() + .map(RevCommit::getId) + .map(AnyObjectId::getName) + .map(CommitHash::new) + .collect(toSet()); + + PersonIdent authorIdent = revCommit.getAuthorIdent(); + PersonIdent committerIdent = revCommit.getCommitterIdent(); + + return new JgitCommit( + hash, + parentHashes, + formatPersonIdent(authorIdent), + authorIdent.getWhen().toInstant(), + formatPersonIdent(committerIdent), + committerIdent.getWhen().toInstant(), + revCommit.getFullMessage()); + } + + public Optional getCommit(CommitHash hash) { + try { + ObjectId commitPtr = repo.resolve(hash.getHash()); + RevCommit revCommit = walk.parseCommit(commitPtr); + return Optional.of(revCommitToJgitCommit(revCommit)); + } catch (IOException e) { + return Optional.empty(); + } + } + + public Stream getAllCommits() throws IOException, GitAPIException { + Iterable logIterable = new Git(repo).log().all().call(); + return StreamSupport.stream(logIterable.spliterator(), false) + .map(JgitCommitWalk::revCommitToJgitCommit); + } + + @Override + public void close() { + walk.close(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/package-info.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/package-info.java index 3869adbed..4467aad17 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/package-info.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/package-info.java @@ -1,6 +1,4 @@ -/** - * The root backend package. - */ +/** The root backend package. */ @ParametersAreNonnullByDefault package de.aaaaaaah.velcom.backend; diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/Admin.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/Admin.java index ff05643c6..73455cccf 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/Admin.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/Admin.java @@ -4,11 +4,10 @@ public class Admin implements Principal { - public Admin() { - } + public Admin() {} - @Override - public String getName() { - return "admin"; - } + @Override + public String getName() { + return "admin"; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/AdminAuthenticator.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/AdminAuthenticator.java index 28f91f112..1fdbb4c06 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/AdminAuthenticator.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/authentication/AdminAuthenticator.java @@ -6,21 +6,21 @@ public class AdminAuthenticator implements Authenticator { - private final String adminToken; + private final String adminToken; - public AdminAuthenticator(String adminToken) { - this.adminToken = adminToken; - } + public AdminAuthenticator(String adminToken) { + this.adminToken = adminToken; + } - @Override - public Optional authenticate(BasicCredentials basicCredentials) { - String username = basicCredentials.getUsername(); - String password = basicCredentials.getPassword(); + @Override + public Optional authenticate(BasicCredentials basicCredentials) { + String username = basicCredentials.getUsername(); + String password = basicCredentials.getPassword(); - if (username.equals("admin") && password.equals(adminToken)) { - return Optional.of(new Admin()); - } + if (username.equals("admin") && password.equals(adminToken)) { + return Optional.of(new Admin()); + } - return Optional.empty(); - } + return Optional.empty(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllDimensionsEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllDimensionsEndpoint.java index 9c689fd0a..7bfd2ccb7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllDimensionsEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllDimensionsEndpoint.java @@ -14,65 +14,69 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -/** - * Endpoint for retrieving a list of all dimensions. - */ +/** Endpoint for retrieving a list of all dimensions. */ @Path("/all-dimensions") @Produces(MediaType.APPLICATION_JSON) public class AllDimensionsEndpoint { - private final DimensionReadAccess dimensionAccess; + private final DimensionReadAccess dimensionAccess; - public AllDimensionsEndpoint(DimensionReadAccess dimensionAccess) { - this.dimensionAccess = dimensionAccess; - } + public AllDimensionsEndpoint(DimensionReadAccess dimensionAccess) { + this.dimensionAccess = dimensionAccess; + } - @GET - @Timed(histogram = true) - public GetReply get() { - Set dimensions = dimensionAccess.getAllDimensions(); - Map runs = dimensionAccess.getRunsPerDimension(); - Map untrackedRuns = dimensionAccess.getUntrackedRunsPerDimension(); - Map unreachableRuns = dimensionAccess.getUnreachableRunsPerDimension(); + @GET + @Timed(histogram = true) + public GetReply get() { + Set dimensions = dimensionAccess.getAllDimensions(); + Map runs = dimensionAccess.getRunsPerDimension(); + Map untrackedRuns = dimensionAccess.getUntrackedRunsPerDimension(); + Map unreachableRuns = dimensionAccess.getUnreachableRunsPerDimension(); - List entries = dimensions.stream() - .map(info -> new DimensionEntry( - JsonDimension.fromDimensionInfo(info), - info.isSignificant(), - runs.getOrDefault(info.getDimension(), 0), - untrackedRuns.getOrDefault(info.getDimension(), 0), - unreachableRuns.getOrDefault(info.getDimension(), 0) - )) - .collect(Collectors.toList()); + List entries = + dimensions.stream() + .map( + info -> + new DimensionEntry( + JsonDimension.fromDimensionInfo(info), + info.isSignificant(), + runs.getOrDefault(info.getDimension(), 0), + untrackedRuns.getOrDefault(info.getDimension(), 0), + unreachableRuns.getOrDefault(info.getDimension(), 0))) + .collect(Collectors.toList()); - return new GetReply(entries); - } + return new GetReply(entries); + } - private static class DimensionEntry { + private static class DimensionEntry { - public final JsonDimension dimension; - public final boolean significant; - public final int runs; - public final int untracked_runs; - public final int unreachable_runs; + public final JsonDimension dimension; + public final boolean significant; + public final int runs; + public final int untracked_runs; + public final int unreachable_runs; - public DimensionEntry(JsonDimension dimension, boolean significant, int runs, - int untracked_runs, int unreachable_runs) { + public DimensionEntry( + JsonDimension dimension, + boolean significant, + int runs, + int untracked_runs, + int unreachable_runs) { - this.dimension = dimension; - this.significant = significant; - this.runs = runs; - this.untracked_runs = untracked_runs; - this.unreachable_runs = unreachable_runs; - } - } + this.dimension = dimension; + this.significant = significant; + this.runs = runs; + this.untracked_runs = untracked_runs; + this.unreachable_runs = unreachable_runs; + } + } - private static class GetReply { + private static class GetReply { - public final List dimensions; + public final List dimensions; - public GetReply(List dimensions) { - this.dimensions = dimensions; - } - } + public GetReply(List dimensions) { + this.dimensions = dimensions; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllReposEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllReposEndpoint.java index 4a8c79fc5..4d651ea8e 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllReposEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/AllReposEndpoint.java @@ -25,73 +25,76 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -/** - * Endpoint for retrieving a list of all current repos. - */ +/** Endpoint for retrieving a list of all current repos. */ @Path("/all-repos") @Produces(MediaType.APPLICATION_JSON) public class AllReposEndpoint { - private final DimensionReadAccess dimensionAccess; - private final RepoReadAccess repoAccess; - private final AvailableDimensionsCache availableDimensionsCache; + private final DimensionReadAccess dimensionAccess; + private final RepoReadAccess repoAccess; + private final AvailableDimensionsCache availableDimensionsCache; - public AllReposEndpoint(DimensionReadAccess dimensionAccess, RepoReadAccess repoAccess, - AvailableDimensionsCache availableDimensionsCache) { + public AllReposEndpoint( + DimensionReadAccess dimensionAccess, + RepoReadAccess repoAccess, + AvailableDimensionsCache availableDimensionsCache) { - this.dimensionAccess = dimensionAccess; - this.repoAccess = repoAccess; - this.availableDimensionsCache = availableDimensionsCache; - } + this.dimensionAccess = dimensionAccess; + this.repoAccess = repoAccess; + this.availableDimensionsCache = availableDimensionsCache; + } - @GET - @Timed(histogram = true) - public GetReply get() { - Collection repos = repoAccess.getAllRepos(); - List repoIds = repos.stream().map(Repo::getId).collect(toList()); - Map> allDimensions = availableDimensionsCache - .getAvailableDimensions(dimensionAccess, repoIds); - Map dimensionInfos = dimensionAccess.getDimensionInfoMap( - allDimensions.values().stream() - .flatMap(Set::stream) - .collect(Collectors.toSet()) - ); + @GET + @Timed(histogram = true) + public GetReply get() { + Collection repos = repoAccess.getAllRepos(); + List repoIds = repos.stream().map(Repo::getId).collect(toList()); + Map> allDimensions = + availableDimensionsCache.getAvailableDimensions(dimensionAccess, repoIds); + Map dimensionInfos = + dimensionAccess.getDimensionInfoMap( + allDimensions.values().stream().flatMap(Set::stream).collect(Collectors.toSet())); - List jsonRepos = repos.stream() - .map(repo -> { - RepoId repoId = repo.getId(); + List jsonRepos = + repos.stream() + .map( + repo -> { + RepoId repoId = repo.getId(); - List branches = repoAccess.getAllBranches(repoId).stream() - .map(JsonBranch::fromBranch) - .collect(toList()); + List branches = + repoAccess.getAllBranches(repoId).stream() + .map(JsonBranch::fromBranch) + .collect(toList()); - List dimensions = allDimensions.get(repoId).stream() - .map(dimension -> JsonDimension.fromDimensionInfo(dimensionInfos.get(dimension))) - .collect(toList()); + List dimensions = + allDimensions.get(repoId).stream() + .map( + dimension -> + JsonDimension.fromDimensionInfo(dimensionInfos.get(dimension))) + .collect(toList()); - return new JsonRepo( - repoId.getId(), - repo.getName(), - repo.getRemoteUrl().getUrl(), - branches, - dimensions, - repo.getGithubInfo() - .map(GithubInfo::getCommentCutoff) - .map(Instant::getEpochSecond) - .orElse(null) - ); - }) - .collect(toList()); + return new JsonRepo( + repoId.getId(), + repo.getName(), + repo.getRemoteUrl().getUrl(), + branches, + dimensions, + repo.getGithubInfo() + .map(GithubInfo::getCommentCutoff) + .map(Instant::getEpochSecond) + .orElse(null)); + }) + .collect(toList()); - return new GetReply(jsonRepos); - } + return new GetReply(jsonRepos); + } - private static class GetReply { + private static class GetReply { - public final List repos; + public final List repos; - public GetReply(List repos) { - this.repos = repos; - } - } + public GetReply(List repos) { + this.repos = repos; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CommitEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CommitEndpoint.java index ecc736fef..188b4cdc4 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CommitEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CommitEndpoint.java @@ -24,115 +24,114 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -/** - * Endpoint for retrieving detailed commit info. - */ +/** Endpoint for retrieving detailed commit info. */ @Path("/commit/{repoid}/{hash}") @Produces(MediaType.APPLICATION_JSON) public class CommitEndpoint { - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final RunCache runCache; - - public CommitEndpoint(BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - RunCache runCache) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.runCache = runCache; - } - - @GET - @Timed(histogram = true) - public GetReply get( - @PathParam("repoid") UUID repoUuid, - @PathParam("hash") String hashString - ) { - RepoId repoId = new RepoId(repoUuid); - CommitHash hash = new CommitHash(hashString); - - FullCommit commit = commitAccess.getFullCommit(repoId, hash); - - List parentCommits = commitAccess.getCommits(repoId, commit.getParentHashes()); - List trackedParents = parentCommits.stream() - .filter(Commit::isTracked) - .map(JsonCommitDescription::fromCommit) - .collect(toList()); - List untrackedParents = parentCommits.stream() - .filter(it -> !it.isTracked()) - .map(JsonCommitDescription::fromCommit) - .collect(toList()); - - List childCommits = commitAccess.getCommits(repoId, commit.getChildHashes()); - List trackedChildren = childCommits.stream() - .filter(Commit::isReachable) - .filter(Commit::isTracked) - .map(JsonCommitDescription::fromCommit) - .collect(toList()); - List untrackedChildren = childCommits.stream() - .filter(Commit::isReachable) - .filter(it -> !it.isTracked()) - .map(JsonCommitDescription::fromCommit) - .collect(toList()); - - List runIds = benchmarkAccess.getAllRunIds(repoId, hash); - List runs = runCache.getRunsInOrder(benchmarkAccess, runIds).stream() - .map(run -> new JsonRunDescription( - run.getId().getId(), - run.getStartTime().getEpochSecond(), - JsonSuccess.fromRunResult(run.getResult()), - JsonSource.fromSource(run.getSource(), commitAccess) - )) - .collect(toList()); - - return new GetReply(new JsonCommit( - commit.getRepoId().getId(), - commit.getHash().getHash(), - commit.isTracked(), - trackedParents, - untrackedParents, - trackedChildren, - untrackedChildren, - commit.getAuthor(), - commit.getAuthorDate().getEpochSecond(), - commit.getCommitter(), - commit.getCommitterDate().getEpochSecond(), - commit.getSummary(), - commit.getMessageWithoutSummary().orElse(null), - runs - )); - } - - private static class GetReply { - - public final JsonCommit commit; - - public GetReply(JsonCommit commit) { - this.commit = commit; - } - } - - @Path("/short") - @GET - @Timed(histogram = true) - public GetShortReply getShort( - @PathParam("repoid") UUID repoUuid, - @PathParam("hash") String hashString - ) { - RepoId repoId = new RepoId(repoUuid); - CommitHash commitHash = new CommitHash(hashString); - - Commit commit = commitAccess.getCommit(repoId, commitHash); - return new GetShortReply(commit.getSummary()); - } - - private static class GetShortReply { - - public final String summary; - - public GetShortReply(String summary) { - this.summary = summary; - } - } + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final RunCache runCache; + + public CommitEndpoint( + BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, RunCache runCache) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.runCache = runCache; + } + + @GET + @Timed(histogram = true) + public GetReply get(@PathParam("repoid") UUID repoUuid, @PathParam("hash") String hashString) { + RepoId repoId = new RepoId(repoUuid); + CommitHash hash = new CommitHash(hashString); + + FullCommit commit = commitAccess.getFullCommit(repoId, hash); + + List parentCommits = commitAccess.getCommits(repoId, commit.getParentHashes()); + List trackedParents = + parentCommits.stream() + .filter(Commit::isTracked) + .map(JsonCommitDescription::fromCommit) + .collect(toList()); + List untrackedParents = + parentCommits.stream() + .filter(it -> !it.isTracked()) + .map(JsonCommitDescription::fromCommit) + .collect(toList()); + + List childCommits = commitAccess.getCommits(repoId, commit.getChildHashes()); + List trackedChildren = + childCommits.stream() + .filter(Commit::isReachable) + .filter(Commit::isTracked) + .map(JsonCommitDescription::fromCommit) + .collect(toList()); + List untrackedChildren = + childCommits.stream() + .filter(Commit::isReachable) + .filter(it -> !it.isTracked()) + .map(JsonCommitDescription::fromCommit) + .collect(toList()); + + List runIds = benchmarkAccess.getAllRunIds(repoId, hash); + List runs = + runCache.getRunsInOrder(benchmarkAccess, runIds).stream() + .map( + run -> + new JsonRunDescription( + run.getId().getId(), + run.getStartTime().getEpochSecond(), + JsonSuccess.fromRunResult(run.getResult()), + JsonSource.fromSource(run.getSource(), commitAccess))) + .collect(toList()); + + return new GetReply( + new JsonCommit( + commit.getRepoId().getId(), + commit.getHash().getHash(), + commit.isTracked(), + trackedParents, + untrackedParents, + trackedChildren, + untrackedChildren, + commit.getAuthor(), + commit.getAuthorDate().getEpochSecond(), + commit.getCommitter(), + commit.getCommitterDate().getEpochSecond(), + commit.getSummary(), + commit.getMessageWithoutSummary().orElse(null), + runs)); + } + + private static class GetReply { + + public final JsonCommit commit; + + public GetReply(JsonCommit commit) { + this.commit = commit; + } + } + + @Path("/short") + @GET + @Timed(histogram = true) + public GetShortReply getShort( + @PathParam("repoid") UUID repoUuid, @PathParam("hash") String hashString) { + RepoId repoId = new RepoId(repoUuid); + CommitHash commitHash = new CommitHash(hashString); + + Commit commit = commitAccess.getCommit(repoId, commitHash); + return new GetShortReply(commit.getSummary()); + } + + private static class GetShortReply { + + public final String summary; + + public GetShortReply(String summary) { + this.summary = summary; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CompareEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CompareEndpoint.java index 62ff8334e..caa31e240 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CompareEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/CompareEndpoint.java @@ -37,104 +37,112 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -/** - * Endpoint for comparing two runs against each other. - */ +/** Endpoint for comparing two runs against each other. */ @Path("/compare/{runid1}/to/{runid2}") @Produces(MediaType.APPLICATION_JSON) public class CompareEndpoint { - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final RunCache runCache; - private final LatestRunCache latestRunCache; - private final RunComparator comparer; - private final SignificanceDetector significanceDetector; - private final SignificanceFactors significanceFactors; - - public CompareEndpoint(BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess, RunCache runCache, LatestRunCache latestRunCache, - RunComparator comparer, SignificanceDetector significanceDetector, - SignificanceFactors significanceFactors) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.runCache = runCache; - this.latestRunCache = latestRunCache; - this.comparer = comparer; - this.significanceDetector = significanceDetector; - this.significanceFactors = significanceFactors; - } - - @GET - @Timed(histogram = true) - public GetReply get( - @PathParam("runid1") UUID runUuid1, - @PathParam("runid2") UUID runUuid2, - @QueryParam("hash1") @Nullable String hash1, - @QueryParam("hash2") @Nullable String hash2, - @QueryParam("all_values") @Nullable Boolean allValuesOptional - ) throws NoSuchRunException { - boolean allValues = (allValuesOptional != null) && allValuesOptional; - - Run run1 = EndpointUtils.getRun(benchmarkAccess, runCache, latestRunCache, runUuid1, hash1); - Run run2 = EndpointUtils.getRun(benchmarkAccess, runCache, latestRunCache, runUuid2, hash2); - Set significantDimensions = dimensionAccess.getSignificantDimensions(); - - RunComparison comparison = comparer.compare(run1, run2); - Optional reasons = significanceDetector - .getSignificance(run2, List.of(run1), significantDimensions); - - Set dimensions = Stream.concat( - comparison.getDimensions().stream(), - reasons.stream().flatMap(it -> it.getDimensions().stream()) - ).collect(toSet()); - Map infos = dimensionAccess.getDimensionInfoMap(dimensions); - - List differences = JsonDimensionDifference - .fromRunComparison(comparison, infos); - - List significantDifferences = reasons.stream() - .map(SignificanceReasons::getSignificantDifferences) - .flatMap(Collection::stream) - .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, infos)) - .collect(toList()); - - List significantFailedDimensions = reasons.stream() - .map(SignificanceReasons::getSignificantFailedDimensions) - .flatMap(Collection::stream) - .map(infos::get) - .map(JsonDimension::fromDimensionInfo) - .collect(toList()); - - return new GetReply( - EndpointUtils.fromRun(dimensionAccess, commitAccess, run1, significanceFactors, allValues), - EndpointUtils.fromRun(dimensionAccess, commitAccess, run2, significanceFactors, allValues), - differences, - significantDifferences, - significantFailedDimensions - ); - } - - private static class GetReply { - - public final JsonRun run1; - public final JsonRun run2; - public final List differences; - public final List significantDifferences; - public final List significantFailedDimensions; - - public GetReply(JsonRun run1, JsonRun run2, List differences, - List significantDifferences, - List significantFailedDimensions) { - - this.run1 = run1; - this.run2 = run2; - this.differences = differences; - this.significantDifferences = significantDifferences; - this.significantFailedDimensions = significantFailedDimensions; - } - } + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final RunCache runCache; + private final LatestRunCache latestRunCache; + private final RunComparator comparer; + private final SignificanceDetector significanceDetector; + private final SignificanceFactors significanceFactors; + + public CompareEndpoint( + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + RunCache runCache, + LatestRunCache latestRunCache, + RunComparator comparer, + SignificanceDetector significanceDetector, + SignificanceFactors significanceFactors) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.runCache = runCache; + this.latestRunCache = latestRunCache; + this.comparer = comparer; + this.significanceDetector = significanceDetector; + this.significanceFactors = significanceFactors; + } + + @GET + @Timed(histogram = true) + public GetReply get( + @PathParam("runid1") UUID runUuid1, + @PathParam("runid2") UUID runUuid2, + @QueryParam("hash1") @Nullable String hash1, + @QueryParam("hash2") @Nullable String hash2, + @QueryParam("all_values") @Nullable Boolean allValuesOptional) + throws NoSuchRunException { + boolean allValues = (allValuesOptional != null) && allValuesOptional; + + Run run1 = EndpointUtils.getRun(benchmarkAccess, runCache, latestRunCache, runUuid1, hash1); + Run run2 = EndpointUtils.getRun(benchmarkAccess, runCache, latestRunCache, runUuid2, hash2); + Set significantDimensions = dimensionAccess.getSignificantDimensions(); + + RunComparison comparison = comparer.compare(run1, run2); + Optional reasons = + significanceDetector.getSignificance(run2, List.of(run1), significantDimensions); + + Set dimensions = + Stream.concat( + comparison.getDimensions().stream(), + reasons.stream().flatMap(it -> it.getDimensions().stream())) + .collect(toSet()); + Map infos = dimensionAccess.getDimensionInfoMap(dimensions); + + List differences = + JsonDimensionDifference.fromRunComparison(comparison, infos); + + List significantDifferences = + reasons.stream() + .map(SignificanceReasons::getSignificantDifferences) + .flatMap(Collection::stream) + .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, infos)) + .collect(toList()); + + List significantFailedDimensions = + reasons.stream() + .map(SignificanceReasons::getSignificantFailedDimensions) + .flatMap(Collection::stream) + .map(infos::get) + .map(JsonDimension::fromDimensionInfo) + .collect(toList()); + + return new GetReply( + EndpointUtils.fromRun(dimensionAccess, commitAccess, run1, significanceFactors, allValues), + EndpointUtils.fromRun(dimensionAccess, commitAccess, run2, significanceFactors, allValues), + differences, + significantDifferences, + significantFailedDimensions); + } + + private static class GetReply { + + public final JsonRun run1; + public final JsonRun run2; + public final List differences; + public final List significantDifferences; + public final List significantFailedDimensions; + + public GetReply( + JsonRun run1, + JsonRun run2, + List differences, + List significantDifferences, + List significantFailedDimensions) { + + this.run1 = run1; + this.run2 = run2; + this.differences = differences; + this.significantDifferences = significantDifferences; + this.significantFailedDimensions = significantFailedDimensions; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DebugEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DebugEndpoint.java index ee8b204ef..adaefe76d 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DebugEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DebugEndpoint.java @@ -11,103 +11,106 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -/** - * Endpoint for debugging. Not specified in the API and can change at any time. - */ +/** Endpoint for debugging. Not specified in the API and can change at any time. */ @Path("/debug") @Produces(MediaType.APPLICATION_JSON) public class DebugEndpoint { - private final BenchmarkReadAccess benchmarkAccess; - private final Dispatcher dispatcher; - - public DebugEndpoint(BenchmarkReadAccess benchmarkAccess, Dispatcher dispatcher) { - this.benchmarkAccess = benchmarkAccess; - this.dispatcher = dispatcher; - } - - @GET - public GetReply get() { - List runnerHashes = dispatcher.getKnownRunners().stream() - .map(r -> new RunnerInfo(r.getName(), r.getVersionHash().orElse(null))) - .collect(Collectors.toList()); - - long tarNotAttached = benchmarkAccess.getAmountOfTarRunsNotAttachedToRepo(); - long tarAttached = benchmarkAccess.getAmountOfTarRunsAttachedToRepo(); - Stats stats = new Stats( - benchmarkAccess.getAmountOfOutdatedRuns(), - tarNotAttached + tarAttached, - tarNotAttached, - tarAttached, - benchmarkAccess.getAmountOfFailedRuns(), - benchmarkAccess.getAmountOfFailedRunErrorMsgChars(), - benchmarkAccess.getAmountOfRunsForUntrackedCommits(), - benchmarkAccess.getAmountOfRunsForUnreachableCommits() - ); - - return new GetReply( - true, - GitProperties.getBuildTime(), - GitProperties.getHash(), - runnerHashes, - stats - ); - } - - private static class GetReply { - - public final boolean safeToUpdate; - public final String buildTime; - public final String backendHash; - public final List runnerHashes; - public final Stats stats; - - public GetReply(boolean safeToUpdate, String buildTime, String backendHash, - List runnerHashes, Stats stats) { - - this.safeToUpdate = safeToUpdate; - this.buildTime = buildTime; - this.backendHash = backendHash; - this.runnerHashes = runnerHashes; - this.stats = stats; - } - } - - private static class RunnerInfo { - - public final String name; - @Nullable - public final String hash; - - public RunnerInfo(String name, @Nullable String hash) { - this.name = name; - this.hash = hash; - } - } - - private static class Stats { - - public final long outdatedRuns; - public final long tarRunsTotal; - public final long tarRunsNotAttachedToRepo; - public final long tarRunsAttachedToRepo; - public final long failedRuns; - public final long failedRunErrorMsgChars; - public final long runsForUntrackedCommits; - public final long runsForUnreachableCommits; - - public Stats(long outdatedRuns, long tarRunsTotal, long tarRunsNotAttachedToRepo, - long tarRunsAttachedToRepo, long failedRuns, long failedRunErrorMsgChars, - long runsForUntrackedCommits, long runsForUnreachableCommits) { - - this.outdatedRuns = outdatedRuns; - this.tarRunsTotal = tarRunsTotal; - this.tarRunsNotAttachedToRepo = tarRunsNotAttachedToRepo; - this.tarRunsAttachedToRepo = tarRunsAttachedToRepo; - this.failedRuns = failedRuns; - this.failedRunErrorMsgChars = failedRunErrorMsgChars; - this.runsForUntrackedCommits = runsForUntrackedCommits; - this.runsForUnreachableCommits = runsForUnreachableCommits; - } - } + private final BenchmarkReadAccess benchmarkAccess; + private final Dispatcher dispatcher; + + public DebugEndpoint(BenchmarkReadAccess benchmarkAccess, Dispatcher dispatcher) { + this.benchmarkAccess = benchmarkAccess; + this.dispatcher = dispatcher; + } + + @GET + public GetReply get() { + List runnerHashes = + dispatcher.getKnownRunners().stream() + .map(r -> new RunnerInfo(r.getName(), r.getVersionHash().orElse(null))) + .collect(Collectors.toList()); + + long tarNotAttached = benchmarkAccess.getAmountOfTarRunsNotAttachedToRepo(); + long tarAttached = benchmarkAccess.getAmountOfTarRunsAttachedToRepo(); + Stats stats = + new Stats( + benchmarkAccess.getAmountOfOutdatedRuns(), + tarNotAttached + tarAttached, + tarNotAttached, + tarAttached, + benchmarkAccess.getAmountOfFailedRuns(), + benchmarkAccess.getAmountOfFailedRunErrorMsgChars(), + benchmarkAccess.getAmountOfRunsForUntrackedCommits(), + benchmarkAccess.getAmountOfRunsForUnreachableCommits()); + + return new GetReply( + true, GitProperties.getBuildTime(), GitProperties.getHash(), runnerHashes, stats); + } + + private static class GetReply { + + public final boolean safeToUpdate; + public final String buildTime; + public final String backendHash; + public final List runnerHashes; + public final Stats stats; + + public GetReply( + boolean safeToUpdate, + String buildTime, + String backendHash, + List runnerHashes, + Stats stats) { + + this.safeToUpdate = safeToUpdate; + this.buildTime = buildTime; + this.backendHash = backendHash; + this.runnerHashes = runnerHashes; + this.stats = stats; + } + } + + private static class RunnerInfo { + + public final String name; + @Nullable public final String hash; + + public RunnerInfo(String name, @Nullable String hash) { + this.name = name; + this.hash = hash; + } + } + + private static class Stats { + + public final long outdatedRuns; + public final long tarRunsTotal; + public final long tarRunsNotAttachedToRepo; + public final long tarRunsAttachedToRepo; + public final long failedRuns; + public final long failedRunErrorMsgChars; + public final long runsForUntrackedCommits; + public final long runsForUnreachableCommits; + + public Stats( + long outdatedRuns, + long tarRunsTotal, + long tarRunsNotAttachedToRepo, + long tarRunsAttachedToRepo, + long failedRuns, + long failedRunErrorMsgChars, + long runsForUntrackedCommits, + long runsForUnreachableCommits) { + + this.outdatedRuns = outdatedRuns; + this.tarRunsTotal = tarRunsTotal; + this.tarRunsNotAttachedToRepo = tarRunsNotAttachedToRepo; + this.tarRunsAttachedToRepo = tarRunsAttachedToRepo; + this.failedRuns = failedRuns; + this.failedRunErrorMsgChars = failedRunErrorMsgChars; + this.runsForUntrackedCommits = runsForUntrackedCommits; + this.runsForUnreachableCommits = runsForUnreachableCommits; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DimensionsEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DimensionsEndpoint.java index bf7751aa9..85fa22f68 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DimensionsEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/DimensionsEndpoint.java @@ -13,30 +13,21 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -/** - * Endpoint for deleting dimensions. - */ +/** Endpoint for deleting dimensions. */ @Path("/dimensions/") @Produces(MediaType.APPLICATION_JSON) public class DimensionsEndpoint { - private final DimensionWriteAccess dimensionAccess; + private final DimensionWriteAccess dimensionAccess; - public DimensionsEndpoint(DimensionWriteAccess dimensionAccess) { - this.dimensionAccess = dimensionAccess; - } + public DimensionsEndpoint(DimensionWriteAccess dimensionAccess) { + this.dimensionAccess = dimensionAccess; + } - @DELETE - @Timed(histogram = true) - public void delete( - @Auth Admin admin, - @NotNull List request - ) { - dimensionAccess.deleteDimensions( - request - .stream() - .map(JsonDimensionId::toDimension) - .collect(Collectors.toList()) - ); - } + @DELETE + @Timed(histogram = true) + public void delete(@Auth Admin admin, @NotNull List request) { + dimensionAccess.deleteDimensions( + request.stream().map(JsonDimensionId::toDimension).collect(Collectors.toList())); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphComparisonEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphComparisonEndpoint.java index efa26ffdf..bf1166e10 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphComparisonEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphComparisonEndpoint.java @@ -40,178 +40,194 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -/** - * Endpoint for getting the repo comparison graph. - */ +/** Endpoint for getting the repo comparison graph. */ @Path("/graph/comparison") @Produces(MediaType.APPLICATION_JSON) public class GraphComparisonEndpoint { - private static final String NO_RUN_FOUND = "N"; - private static final String NO_MEASUREMENT_FOUND = "O"; - private static final String RUN_FAILED = "R"; - private static final String MEASUREMENT_FAILED = "M"; - - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - - public GraphComparisonEndpoint(BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - } - - @GET - @Timed(histogram = true) - public GetReply get( - @QueryParam("repos") @NotNull String reposStr, - @QueryParam("start_time") @Nullable Long startTimeEpoch, - @QueryParam("end_time") @Nullable Long endTimeEpoch, - @QueryParam("duration") @Nullable Integer durationInSeconds, - @QueryParam("dimension") @NotNull String dimensionStr - ) { - // Parse dimension - Set dimensionSet = EndpointUtils.parseDimensions(dimensionStr); - if (dimensionSet.size() != 1) { - throw new InvalidQueryParamsException("exactly one dimension must be specified"); - } - - Dimension dimension = dimensionSet.iterator().next(); - dimensionAccess.guardDimensionExists(dimension); - - // Figure out the start and end time - Pair startAndEndTime = EndpointUtils - .getStartAndEndTime(startTimeEpoch, endTimeEpoch, durationInSeconds); - Instant startTime = startAndEndTime.getFirst(); - Instant endTime = startAndEndTime.getSecond(); - if (startTime != null && endTime != null && startTime.isAfter(endTime)) { - throw new InvalidQueryParamsException("start time must be earlier than end time"); - } - - // Retrieve the graph information. - // - // You are standing in front of a dense forest. The forest is full of streams. If you venture - // any further, you might slip. Do you want to continue? [Y/n] - List repos = EndpointUtils.parseRepos(reposStr) - .entrySet() - .stream() - .map(entry -> { - // Iterating over all repos (and associated branches) from the request... - RepoId repoId = entry.getKey(); - Set branches = entry.getValue(); - - // Find the commits that will later be displayed in the graph - List commits = commitAccess.getCommitsBetween(repoId, branches, startTime, endTime); - List fullCommits = commitAccess.promoteCommits(commits); - Map fullCommitsByHash = fullCommits.stream() - .collect(toMap(Commit::getHash, it -> it)); - - fullCommits = EndpointUtils.topologicalSort(fullCommits, fullCommitsByHash); - fullCommits.sort(Comparator.comparing(Commit::getCommitterDate)); - - // Find the latest run belonging to each commit - Set latestRunIds = new HashSet<>( - benchmarkAccess.getLatestRunIds(repoId, fullCommitsByHash.keySet()).values()); - Map runs = benchmarkAccess.getRuns(latestRunIds).stream() - .collect(toMap( - run -> run.getSource().getLeft().map(CommitSource::getHash).orElse(null), - run -> run - )); - - // Collect the commit and run information as JsonGraphCommits - List graphCommits = fullCommits.stream() - .map(commit -> new JsonGraphCommit( - commit.getHashAsString(), - commit.getParentHashes().stream().map(CommitHash::getHash).collect(toList()), - commit.getAuthor(), - commit.getCommitterDate().getEpochSecond(), - commit.getSummary(), - getValueOfCommit(commit, dimension, runs) - )) - .collect(toList()); - - // We've now collected all information required for this particular repository - return new JsonGraphRepo(repoId.getId(), graphCommits); - }) - .collect(toList()); - - DimensionInfo dimensionInfo = dimensionAccess.getDimensionInfo(dimension); - JsonDimension jsonDimension = JsonDimension.fromDimensionInfo(dimensionInfo); - - return new GetReply(jsonDimension, repos); - } - - private static Object getValueOfCommit(Commit commit, Dimension dimension, - Map runs) { - - Run run = runs.get(commit.getHash()); - if (run == null) { - return NO_RUN_FOUND; - } - - Optional> result = run.getResult().getRight(); - if (result.isEmpty()) { - return RUN_FAILED; - } - - Optional measurement = result.get().stream() - .filter(it -> it.getDimension().equals(dimension)) - .findFirst(); - if (measurement.isEmpty()) { - return NO_MEASUREMENT_FOUND; - } - - Optional values = measurement.get().getContent().getRight(); - if (values.isEmpty()) { - return MEASUREMENT_FAILED; - } - - return values.get().getAverageValue(); - } - - private static class GetReply { - - public final JsonDimension dimension; - public final List repos; - - public GetReply(JsonDimension dimension, List repos) { - this.dimension = dimension; - this.repos = repos; - } - } - - private static class JsonGraphRepo { - - public final UUID repoId; - public final List commits; - - public JsonGraphRepo(UUID repoId, List commits) { - this.repoId = repoId; - this.commits = commits; - } - } - - private static class JsonGraphCommit { - - public final String hash; - public final List parents; - public final String author; - public final long committerDate; - public final String summary; - public final Object value; - - public JsonGraphCommit(String hash, List parents, String author, long committerDate, - String summary, Object value) { - - this.hash = hash; - this.parents = parents; - this.author = author; - this.committerDate = committerDate; - this.summary = summary; - this.value = value; - } - } + private static final String NO_RUN_FOUND = "N"; + private static final String NO_MEASUREMENT_FOUND = "O"; + private static final String RUN_FAILED = "R"; + private static final String MEASUREMENT_FAILED = "M"; + + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + + public GraphComparisonEndpoint( + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + } + + @GET + @Timed(histogram = true) + public GetReply get( + @QueryParam("repos") @NotNull String reposStr, + @QueryParam("start_time") @Nullable Long startTimeEpoch, + @QueryParam("end_time") @Nullable Long endTimeEpoch, + @QueryParam("duration") @Nullable Integer durationInSeconds, + @QueryParam("dimension") @NotNull String dimensionStr) { + // Parse dimension + Set dimensionSet = EndpointUtils.parseDimensions(dimensionStr); + if (dimensionSet.size() != 1) { + throw new InvalidQueryParamsException("exactly one dimension must be specified"); + } + + Dimension dimension = dimensionSet.iterator().next(); + dimensionAccess.guardDimensionExists(dimension); + + // Figure out the start and end time + Pair startAndEndTime = + EndpointUtils.getStartAndEndTime(startTimeEpoch, endTimeEpoch, durationInSeconds); + Instant startTime = startAndEndTime.getFirst(); + Instant endTime = startAndEndTime.getSecond(); + if (startTime != null && endTime != null && startTime.isAfter(endTime)) { + throw new InvalidQueryParamsException("start time must be earlier than end time"); + } + + // Retrieve the graph information. + // + // You are standing in front of a dense forest. The forest is full of streams. If you venture + // any further, you might slip. Do you want to continue? [Y/n] + List repos = + EndpointUtils.parseRepos(reposStr).entrySet().stream() + .map( + entry -> { + // Iterating over all repos (and associated branches) from the request... + RepoId repoId = entry.getKey(); + Set branches = entry.getValue(); + + // Find the commits that will later be displayed in the graph + List commits = + commitAccess.getCommitsBetween(repoId, branches, startTime, endTime); + List fullCommits = commitAccess.promoteCommits(commits); + Map fullCommitsByHash = + fullCommits.stream().collect(toMap(Commit::getHash, it -> it)); + + fullCommits = EndpointUtils.topologicalSort(fullCommits, fullCommitsByHash); + fullCommits.sort(Comparator.comparing(Commit::getCommitterDate)); + + // Find the latest run belonging to each commit + Set latestRunIds = + new HashSet<>( + benchmarkAccess + .getLatestRunIds(repoId, fullCommitsByHash.keySet()) + .values()); + Map runs = + benchmarkAccess.getRuns(latestRunIds).stream() + .collect( + toMap( + run -> + run.getSource() + .getLeft() + .map(CommitSource::getHash) + .orElse(null), + run -> run)); + + // Collect the commit and run information as JsonGraphCommits + List graphCommits = + fullCommits.stream() + .map( + commit -> + new JsonGraphCommit( + commit.getHashAsString(), + commit.getParentHashes().stream() + .map(CommitHash::getHash) + .collect(toList()), + commit.getAuthor(), + commit.getCommitterDate().getEpochSecond(), + commit.getSummary(), + getValueOfCommit(commit, dimension, runs))) + .collect(toList()); + + // We've now collected all information required for this particular repository + return new JsonGraphRepo(repoId.getId(), graphCommits); + }) + .collect(toList()); + + DimensionInfo dimensionInfo = dimensionAccess.getDimensionInfo(dimension); + JsonDimension jsonDimension = JsonDimension.fromDimensionInfo(dimensionInfo); + + return new GetReply(jsonDimension, repos); + } + + private static Object getValueOfCommit( + Commit commit, Dimension dimension, Map runs) { + + Run run = runs.get(commit.getHash()); + if (run == null) { + return NO_RUN_FOUND; + } + + Optional> result = run.getResult().getRight(); + if (result.isEmpty()) { + return RUN_FAILED; + } + + Optional measurement = + result.get().stream().filter(it -> it.getDimension().equals(dimension)).findFirst(); + if (measurement.isEmpty()) { + return NO_MEASUREMENT_FOUND; + } + + Optional values = measurement.get().getContent().getRight(); + if (values.isEmpty()) { + return MEASUREMENT_FAILED; + } + + return values.get().getAverageValue(); + } + + private static class GetReply { + + public final JsonDimension dimension; + public final List repos; + + public GetReply(JsonDimension dimension, List repos) { + this.dimension = dimension; + this.repos = repos; + } + } + + private static class JsonGraphRepo { + + public final UUID repoId; + public final List commits; + + public JsonGraphRepo(UUID repoId, List commits) { + this.repoId = repoId; + this.commits = commits; + } + } + + private static class JsonGraphCommit { + + public final String hash; + public final List parents; + public final String author; + public final long committerDate; + public final String summary; + public final Object value; + + public JsonGraphCommit( + String hash, + List parents, + String author, + long committerDate, + String summary, + Object value) { + + this.hash = hash; + this.parents = parents; + this.author = author; + this.committerDate = committerDate; + this.summary = summary; + this.value = value; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphDetailEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphDetailEndpoint.java index c8f7838c9..cb170f722 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphDetailEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphDetailEndpoint.java @@ -41,199 +41,211 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -/** - * Endpoint for getting the detail graph. - */ +/** Endpoint for getting the detail graph. */ @Path("/graph/detail/{repoid}") @Produces(MediaType.APPLICATION_JSON) public class GraphDetailEndpoint { - private static final String NO_RUN_FOUND = "N"; - private static final String NO_MEASUREMENT_FOUND = "O"; - private static final String RUN_FAILED = "R"; - private static final String MEASUREMENT_FAILED = "M"; - - private final CommitReadAccess commitAccess; - private final BenchmarkReadAccess benchmarkAccess; - private final DimensionReadAccess dimensionAccess; - private final RepoReadAccess repoAccess; - private final RunCache runCache; - private final LatestRunCache latestRunCache; - - public GraphDetailEndpoint(CommitReadAccess commitAccess, BenchmarkReadAccess benchmarkAccess, - DimensionReadAccess dimensionAccess, RepoReadAccess repoAccess, RunCache runCache, - LatestRunCache latestRunCache) { - - this.commitAccess = commitAccess; - this.benchmarkAccess = benchmarkAccess; - this.dimensionAccess = dimensionAccess; - this.repoAccess = repoAccess; - this.runCache = runCache; - this.latestRunCache = latestRunCache; - } - - @GET - @Timed(histogram = true) - public GetReply get( - @PathParam("repoid") UUID repoUuid, - @QueryParam("start_time") @Nullable Long startTimeEpoch, - @QueryParam("end_time") @Nullable Long endTimeEpoch, - @QueryParam("duration") @Nullable Integer durationInSeconds, - @QueryParam("dimensions") @NotNull String dimensionStr - ) throws NoSuchRepoException { - // Figure out tracked branches - RepoId repoId = new RepoId(repoUuid); - repoAccess.guardRepoExists(repoId); - - // Figure out which dimensions there actually are, and in which order we'll return them - Set potentialDimensions = EndpointUtils.parseDimensions(dimensionStr); - Set allDimensions = dimensionAccess.getAllDimensions(); - List existingDimensions = allDimensions.stream() - .filter(info -> potentialDimensions.contains(info.getDimension())) - .sorted(Comparator.comparing(DimensionInfo::getDimension)) - .collect(Collectors.toList()); - - // Figure out the start and end time - Pair startAndEndTime = EndpointUtils - .getStartAndEndTime(startTimeEpoch, endTimeEpoch, durationInSeconds); - Instant startTime = startAndEndTime.getFirst(); - Instant endTime = startAndEndTime.getSecond(); - if (startTime != null && endTime != null && startTime.isAfter(endTime)) { - throw new InvalidQueryParamsException("start time must be earlier than end time"); - } - - // Find the commits that will later be displayed in the graph - List commits = commitAccess.getTrackedCommitsBetween(repoId, startTime, endTime); - List fullCommits = commitAccess.promoteCommits(commits); - Map fullCommitsByHash = fullCommits.stream() - .collect(toMap(Commit::getHash, it -> it)); - - fullCommits = EndpointUtils.topologicalSort(fullCommits, fullCommitsByHash); - fullCommits.sort(Comparator.comparing(Commit::getCommitterDate)); - - // Obtain the relevant runs - Map runs = latestRunCache - .getLatestRuns(benchmarkAccess, runCache, repoId, fullCommitsByHash.keySet()); - - // Finally, put everything together. - List jsonDimensions = existingDimensions.stream() - .map(JsonDimension::fromDimensionInfo) - .collect(Collectors.toList()); - List jsonGraphCommits = fullCommits.stream() - .map(commit -> new JsonGraphCommit( - commit.getHash().getHash(), - commit.getParentHashes().stream().map(CommitHash::getHash).collect(Collectors.toList()), - commit.getAuthor(), - commit.getCommitterDate().getEpochSecond(), - commit.getSummary(), - extractValuesFromCommit(existingDimensions, runs, commit) - )) - .collect(Collectors.toList()); - - return new GetReply(jsonDimensions, jsonGraphCommits); - } - - private static List extractValuesFromCommit(List dimensions, - Map runs, Commit commit) { - - Run run = runs.get(commit.getHash()); - if (run == null) { - return createListFullOf(dimensions, NO_RUN_FOUND); - } - - Optional> optionalMeasurements = run.getResult().getRight(); - if (optionalMeasurements.isEmpty()) { - return createListFullOf(dimensions, RUN_FAILED); - } - - Map measurements = optionalMeasurements.get().stream() - .collect(toMap(Measurement::getDimension, m -> m)); - - return dimensions.stream() - .map(dim -> { - Measurement measurement = measurements.get(dim.getDimension()); - if (measurement == null) { - return NO_MEASUREMENT_FOUND; - } - - Optional values = measurement.getContent().getRight(); - if (values.isEmpty()) { - return MEASUREMENT_FAILED; - } - - return values.get().getAverageValue(); - }) - .collect(Collectors.toList()); - } - - private static List createListFullOf(List dimensions, String string) { - return dimensions.stream() - .map(dim -> string) - .collect(Collectors.toList()); - } - - private static class GetReply { - - private final List dimensions; - private final List commits; - - public GetReply(List dimensions, List commits) { - this.dimensions = dimensions; - this.commits = commits; - } - - public List getDimensions() { - return dimensions; - } - - public List getCommits() { - return commits; - } - } - - private static class JsonGraphCommit { - - private final String hash; - private final List parents; - private final String author; - private final long committerDate; - private final String summary; - // A list of Doubles and Strings - private final List values; - - public JsonGraphCommit(String hash, List parents, String author, long comitterDate, - String summary, List values) { - - this.hash = hash; - this.parents = parents; - this.author = author; - this.committerDate = comitterDate; - this.summary = summary; - this.values = values; - } - - public String getHash() { - return hash; - } - - public List getParents() { - return parents; - } - - public String getAuthor() { - return author; - } - - public long getCommitterDate() { - return committerDate; - } - - public String getSummary() { - return summary; - } - - public List getValues() { - return values; - } - } + private static final String NO_RUN_FOUND = "N"; + private static final String NO_MEASUREMENT_FOUND = "O"; + private static final String RUN_FAILED = "R"; + private static final String MEASUREMENT_FAILED = "M"; + + private final CommitReadAccess commitAccess; + private final BenchmarkReadAccess benchmarkAccess; + private final DimensionReadAccess dimensionAccess; + private final RepoReadAccess repoAccess; + private final RunCache runCache; + private final LatestRunCache latestRunCache; + + public GraphDetailEndpoint( + CommitReadAccess commitAccess, + BenchmarkReadAccess benchmarkAccess, + DimensionReadAccess dimensionAccess, + RepoReadAccess repoAccess, + RunCache runCache, + LatestRunCache latestRunCache) { + + this.commitAccess = commitAccess; + this.benchmarkAccess = benchmarkAccess; + this.dimensionAccess = dimensionAccess; + this.repoAccess = repoAccess; + this.runCache = runCache; + this.latestRunCache = latestRunCache; + } + + @GET + @Timed(histogram = true) + public GetReply get( + @PathParam("repoid") UUID repoUuid, + @QueryParam("start_time") @Nullable Long startTimeEpoch, + @QueryParam("end_time") @Nullable Long endTimeEpoch, + @QueryParam("duration") @Nullable Integer durationInSeconds, + @QueryParam("dimensions") @NotNull String dimensionStr) + throws NoSuchRepoException { + // Figure out tracked branches + RepoId repoId = new RepoId(repoUuid); + repoAccess.guardRepoExists(repoId); + + // Figure out which dimensions there actually are, and in which order we'll return them + Set potentialDimensions = EndpointUtils.parseDimensions(dimensionStr); + Set allDimensions = dimensionAccess.getAllDimensions(); + List existingDimensions = + allDimensions.stream() + .filter(info -> potentialDimensions.contains(info.getDimension())) + .sorted(Comparator.comparing(DimensionInfo::getDimension)) + .collect(Collectors.toList()); + + // Figure out the start and end time + Pair startAndEndTime = + EndpointUtils.getStartAndEndTime(startTimeEpoch, endTimeEpoch, durationInSeconds); + Instant startTime = startAndEndTime.getFirst(); + Instant endTime = startAndEndTime.getSecond(); + if (startTime != null && endTime != null && startTime.isAfter(endTime)) { + throw new InvalidQueryParamsException("start time must be earlier than end time"); + } + + // Find the commits that will later be displayed in the graph + List commits = commitAccess.getTrackedCommitsBetween(repoId, startTime, endTime); + List fullCommits = commitAccess.promoteCommits(commits); + Map fullCommitsByHash = + fullCommits.stream().collect(toMap(Commit::getHash, it -> it)); + + fullCommits = EndpointUtils.topologicalSort(fullCommits, fullCommitsByHash); + fullCommits.sort(Comparator.comparing(Commit::getCommitterDate)); + + // Obtain the relevant runs + Map runs = + latestRunCache.getLatestRuns(benchmarkAccess, runCache, repoId, fullCommitsByHash.keySet()); + + // Finally, put everything together. + List jsonDimensions = + existingDimensions.stream() + .map(JsonDimension::fromDimensionInfo) + .collect(Collectors.toList()); + List jsonGraphCommits = + fullCommits.stream() + .map( + commit -> + new JsonGraphCommit( + commit.getHash().getHash(), + commit.getParentHashes().stream() + .map(CommitHash::getHash) + .collect(Collectors.toList()), + commit.getAuthor(), + commit.getCommitterDate().getEpochSecond(), + commit.getSummary(), + extractValuesFromCommit(existingDimensions, runs, commit))) + .collect(Collectors.toList()); + + return new GetReply(jsonDimensions, jsonGraphCommits); + } + + private static List extractValuesFromCommit( + List dimensions, Map runs, Commit commit) { + + Run run = runs.get(commit.getHash()); + if (run == null) { + return createListFullOf(dimensions, NO_RUN_FOUND); + } + + Optional> optionalMeasurements = run.getResult().getRight(); + if (optionalMeasurements.isEmpty()) { + return createListFullOf(dimensions, RUN_FAILED); + } + + Map measurements = + optionalMeasurements.get().stream().collect(toMap(Measurement::getDimension, m -> m)); + + return dimensions.stream() + .map( + dim -> { + Measurement measurement = measurements.get(dim.getDimension()); + if (measurement == null) { + return NO_MEASUREMENT_FOUND; + } + + Optional values = measurement.getContent().getRight(); + if (values.isEmpty()) { + return MEASUREMENT_FAILED; + } + + return values.get().getAverageValue(); + }) + .collect(Collectors.toList()); + } + + private static List createListFullOf(List dimensions, String string) { + return dimensions.stream().map(dim -> string).collect(Collectors.toList()); + } + + private static class GetReply { + + private final List dimensions; + private final List commits; + + public GetReply(List dimensions, List commits) { + this.dimensions = dimensions; + this.commits = commits; + } + + public List getDimensions() { + return dimensions; + } + + public List getCommits() { + return commits; + } + } + + private static class JsonGraphCommit { + + private final String hash; + private final List parents; + private final String author; + private final long committerDate; + private final String summary; + // A list of Doubles and Strings + private final List values; + + public JsonGraphCommit( + String hash, + List parents, + String author, + long comitterDate, + String summary, + List values) { + + this.hash = hash; + this.parents = parents; + this.author = author; + this.committerDate = comitterDate; + this.summary = summary; + this.values = values; + } + + public String getHash() { + return hash; + } + + public List getParents() { + return parents; + } + + public String getAuthor() { + return author; + } + + public long getCommitterDate() { + return committerDate; + } + + public String getSummary() { + return summary; + } + + public List getValues() { + return values; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphStatusComparisonEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphStatusComparisonEndpoint.java index 19e12efdc..fe32f45a7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphStatusComparisonEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/GraphStatusComparisonEndpoint.java @@ -28,97 +28,105 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -/** - * Endpoint for getting the repo status comparison graph. - */ +/** Endpoint for getting the repo status comparison graph. */ @Path("/graph/status-comparison") @Produces(MediaType.APPLICATION_JSON) public class GraphStatusComparisonEndpoint { - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final RepoReadAccess repoAccess; - private final SignificanceFactors significanceFactors; - - public GraphStatusComparisonEndpoint(BenchmarkReadAccess benchmarkAccess, - CommitReadAccess commitAccess, DimensionReadAccess dimensionAccess, RepoReadAccess repoAccess, - SignificanceFactors significanceFactors) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.repoAccess = repoAccess; - this.significanceFactors = significanceFactors; - } - - @GET - @Timed(histogram = true) - public GetReply get(@QueryParam("repos") @NotNull String reposStr) { - List runs = EndpointUtils.parseRepos(reposStr) - .entrySet() - .stream() - .flatMap(entry -> { - RepoId repoId = entry.getKey(); - List branches = repoAccess.getAllBranches(repoId) - .stream() - .filter(branch -> entry.getValue().contains(branch.getName())) - .collect(toList()); - - if (branches.isEmpty()) { - return Stream.of(); - } - - List commitHashes = branches.stream() - .map(Branch::getLatestCommitHash) - .collect(toList()); - return commitAccess.getCommits(repoId, commitHashes) - .stream() - .max(Comparator.comparing(Commit::getCommitterDate)) - .stream(); - }) - .map(commit -> { - Optional run = benchmarkAccess.getLatestRunId(commit.getRepoId(), commit.getHash()) - .map(benchmarkAccess::getRun); - return new Pair<>(commit, run); - }) - .map(pair -> { - Commit commit = pair.getFirst(); - Optional jsonRun = pair.getSecond() - .map(run -> EndpointUtils - .fromRun(dimensionAccess, commitAccess, run, significanceFactors, false)); - - return new JsonRepoEntry( - commit.getRepoId().getIdAsString(), - commit.getHashAsString(), - jsonRun.orElse(null) - ); - }) - .collect(toList()); - - return new GetReply(runs); - } - - private static class GetReply { - - public final List runs; - - public GetReply(List runs) { - this.runs = runs; - } - } - - private static class JsonRepoEntry { - - public final String repoId; - public final String commitHash; - @Nullable - public final JsonRun run; - - public JsonRepoEntry(String repoId, String commitHash, @Nullable JsonRun run) { - this.repoId = repoId; - this.commitHash = commitHash; - this.run = run; - } - } + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final RepoReadAccess repoAccess; + private final SignificanceFactors significanceFactors; + + public GraphStatusComparisonEndpoint( + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + RepoReadAccess repoAccess, + SignificanceFactors significanceFactors) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.repoAccess = repoAccess; + this.significanceFactors = significanceFactors; + } + + @GET + @Timed(histogram = true) + public GetReply get(@QueryParam("repos") @NotNull String reposStr) { + List runs = + EndpointUtils.parseRepos(reposStr).entrySet().stream() + .flatMap( + entry -> { + RepoId repoId = entry.getKey(); + List branches = + repoAccess.getAllBranches(repoId).stream() + .filter(branch -> entry.getValue().contains(branch.getName())) + .collect(toList()); + + if (branches.isEmpty()) { + return Stream.of(); + } + + List commitHashes = + branches.stream().map(Branch::getLatestCommitHash).collect(toList()); + return commitAccess.getCommits(repoId, commitHashes).stream() + .max(Comparator.comparing(Commit::getCommitterDate)) + .stream(); + }) + .map( + commit -> { + Optional run = + benchmarkAccess + .getLatestRunId(commit.getRepoId(), commit.getHash()) + .map(benchmarkAccess::getRun); + return new Pair<>(commit, run); + }) + .map( + pair -> { + Commit commit = pair.getFirst(); + Optional jsonRun = + pair.getSecond() + .map( + run -> + EndpointUtils.fromRun( + dimensionAccess, + commitAccess, + run, + significanceFactors, + false)); + + return new JsonRepoEntry( + commit.getRepoId().getIdAsString(), + commit.getHashAsString(), + jsonRun.orElse(null)); + }) + .collect(toList()); + + return new GetReply(runs); + } + + private static class GetReply { + + public final List runs; + + public GetReply(List runs) { + this.runs = runs; + } + } + + private static class JsonRepoEntry { + + public final String repoId; + public final String commitHash; + @Nullable public final JsonRun run; + + public JsonRepoEntry(String repoId, String commitHash, @Nullable JsonRun run) { + this.repoId = repoId; + this.commitHash = commitHash; + this.run = run; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/ListenerEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/ListenerEndpoint.java index 7c471ccb5..f65f64e63 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/ListenerEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/ListenerEndpoint.java @@ -13,16 +13,16 @@ @Produces(MediaType.APPLICATION_JSON) public class ListenerEndpoint { - private final Listener listener; + private final Listener listener; - public ListenerEndpoint(Listener listener) { - this.listener = listener; - } + public ListenerEndpoint(Listener listener) { + this.listener = listener; + } - @POST - @Path("/fetch-all") - @Timed(histogram = true) - public void post(@Auth Admin admin) { - listener.updateAllRepos(); - } + @POST + @Path("/fetch-all") + @Timed(histogram = true) + public void post(@Auth Admin admin) { + listener.updateAllRepos(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/QueueEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/QueueEndpoint.java index ab0a9a240..d979a9315 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/QueueEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/QueueEndpoint.java @@ -55,295 +55,306 @@ @Produces(MediaType.APPLICATION_JSON) public class QueueEndpoint { - private static final String AUTHOR_NAME_ADMIN = "Admin"; - - private final CommitReadAccess commitReadAccess; - private final RepoReadAccess repoReadAccess; - private final Queue queue; - private final IDispatcher dispatcher; - - public QueueEndpoint(CommitReadAccess commitReadAccess, RepoReadAccess repoReadAccess, - Queue queue, IDispatcher dispatcher) { - - this.commitReadAccess = commitReadAccess; - this.repoReadAccess = repoReadAccess; - this.queue = queue; - this.dispatcher = dispatcher; - } - - @GET - @Timed(histogram = true) - public GetQueueReply getQueue() { - List tasks = queue.getAllTasksInOrder(); - - Map> hashPerRepo = tasks.stream() - // do - // repoId <- taskRepoId t - // hash <- getHash <$> getLeft (taskSource t) - // pure (repoId, hash) - .flatMap(task -> task.getSource().getLeft() - .flatMap(cs -> task.getRepoId().map(rid -> new Pair<>(rid, cs.getHash()))) - .stream()) - .collect(Collectors.groupingBy( - Pair::getFirst, - Collectors.mapping(Pair::getSecond, Collectors.toList()) - )); - - Map> commitPerHashPerRepo = hashPerRepo.entrySet().stream() - .collect(toMap( - Entry::getKey, - it -> commitReadAccess.getCommits(it.getKey(), it.getValue()).stream() - .collect(toMap(Commit::getHash, commit -> commit)) - )); - - List jsonTasks = tasks.stream() - .map(task -> { - JsonSource source = task.getSource() - .mapLeft(it -> { - RepoId repoId = it.getRepoId(); - CommitHash hash = it.getHash(); - return commitPerHashPerRepo.get(repoId) - .getOrDefault(hash, Commit.placeholder(repoId, hash)); - }) - .consume(JsonSource::fromCommit, JsonSource::fromTarSource); - return JsonTask.fromTask(task, source); - }) - .collect(Collectors.toList()); - - List worker = dispatcher.getKnownRunners() - .stream() - .map(JsonRunner::fromKnownRunner) - .collect(Collectors.toList()); - - return new GetQueueReply(jsonTasks, worker); - } - - @DELETE - @Timed(histogram = true) - public void emptyQueue(@Auth Admin admin) throws NoSuchTaskException { - queue.deleteAllTasks(); - } - - @DELETE - @Path("{taskid}") - @Timed(histogram = true) - public void deleteTask(@Auth Admin admin, @PathParam("taskid") UUID taskUuid) - throws NoSuchTaskException { - - TaskId taskId = new TaskId(taskUuid); - queue.guardTaskExists(taskId); - queue.deleteTasks(List.of(taskId)); - } - - @PATCH - @Path("{taskid}") - @Timed(histogram = true) - public void patchTask(@Auth Admin admin, @PathParam("taskid") UUID taskUuid) - throws NoSuchTaskException { - - TaskId taskId = new TaskId(taskUuid); - queue.guardTaskExists(taskId); - queue.prioritizeTask(taskId, TaskPriority.MANUAL); - } - - @POST - @Path("commit/{repoid}/{hash}") - @Timed(histogram = true) - public PostCommitReply addCommit( - @Auth Admin admin, - @PathParam("repoid") UUID repoUuid, - @PathParam("hash") String commitHashString - ) throws NoSuchRepoException { - RepoId repoId = new RepoId(repoUuid); - CommitHash commitHash = new CommitHash(commitHashString); - - repoReadAccess.guardRepoExists(repoId); - commitReadAccess.guardCommitExists(repoId, commitHash); - // If at any point between these checks and inserting the task the repo is deleted, JOOQ will - // throw a DataAccessException (I believe) and this endpoint will return a 500. But the chance - // of that happening is low enough that putting in the effort to perform the checks in an atomic - // way is not worth it. - - final Optional insertedTask = queue - .addCommit(AUTHOR_NAME_ADMIN, repoId, commitHash, TaskPriority.MANUAL); - - if (insertedTask.isEmpty()) { - throw new TaskAlreadyExistsException(commitHash, repoId); - } - - return new PostCommitReply(JsonTask.fromTask(insertedTask.get(), commitReadAccess)); - } - - - @POST - @Path("commit/{repoid}/{hash}/one-up") - @Timed(histogram = true) - public PostOneUpReply PostOneUpReply( - @Auth Admin admin, - @PathParam("repoid") UUID repoUuid, - @PathParam("hash") String commitHashString - ) throws NoSuchRepoException { - RepoId repoId = new RepoId(repoUuid); - CommitHash rootHash = new CommitHash(commitHashString); - - commitReadAccess.guardCommitExists(repoId, rootHash); - - List hashes = commitReadAccess.getDescendantCommits(repoId, rootHash); - - queue.addCommits(AUTHOR_NAME_ADMIN, repoId, hashes, TaskPriority.LISTENER); - - List tasks = queue.getAllTasksInOrder().stream() - .filter(it -> it.getRepoId().map(repoId::equals).orElse(false)) - .filter(it -> it.getCommitHash().isPresent()) - .filter(it -> hashes.contains(it.getCommitHash().get())) - .map(task -> JsonTask.fromTask(task, commitReadAccess)) - .collect(Collectors.toList()); - - return new PostOneUpReply(tasks); - } - - @POST - @Path("upload/tar") - @Consumes(MediaType.MULTIPART_FORM_DATA) - @Timed(histogram = true) - public UploadTarReply uploadTar( - @Auth Admin admin, - @NotNull @FormDataParam("description") String description, - @Nullable @FormDataParam("repo_id") UUID repoUuid, - @NotNull @FormDataParam("file") InputStream inputStream, - @NotNull @FormDataParam("file") FormDataContentDisposition fileDisposition - ) throws IOException { - Optional repoId = Optional.ofNullable(repoUuid).map(RepoId::new); - - final InputStream uncompressedInput = - fileDisposition.getFileName().endsWith(".tar.gz") - ? new GZIPInputStream(inputStream) - : inputStream; - - try (uncompressedInput) { - Task task = queue.addTar( - AUTHOR_NAME_ADMIN, - TaskPriority.MANUAL, - repoId.orElse(null), - description, - uncompressedInput - ); - - return new UploadTarReply(JsonTask.fromTask(task, commitReadAccess)); - } catch (TaskCreationException e) { - Status status = e.isOurFault() ? Status.INTERNAL_SERVER_ERROR : Status.BAD_REQUEST; - throw new WebApplicationException(e.getMessage(), status); - } - } - - @Path("task/{taskid}") - @GET - @Timed(histogram = true) - public GetTaskInfoReply getTask(@PathParam("taskid") UUID taskId) { - int indexOfTask = 0; - Optional foundTask = Optional.empty(); - for (Task task : queue.getAllTasksInOrder()) { - if (task.getIdAsUuid().equals(taskId)) { - foundTask = Optional.of(task); - break; - } - indexOfTask++; - } - - if (foundTask.isEmpty()) { - throw new WebApplicationException(Status.NOT_FOUND); - } - - Long runningSince = dispatcher.getKnownRunners().stream() - .filter(it -> it.getCurrentTask().isPresent()) - .filter(it -> it.getCurrentTask().get().getId().getId().equals(taskId)) - .findAny() - .flatMap(KnownRunner::getWorkingSince) - .map(Instant::getEpochSecond) - .orElse(null); - - return new GetTaskInfoReply(JsonTask.fromTask( - foundTask.get(), commitReadAccess), - indexOfTask, - runningSince - ); - } - - @Path("task/{taskid}/progress") - @GET - @Timed(histogram = true) - public GetTaskOutputReply getRunnerOutput(@PathParam("taskid") UUID taskId) { - Optional lastOutputLinesOpt = dispatcher.findLinesForTask(taskId); - - if (lastOutputLinesOpt.isEmpty()) { - throw new WebApplicationException(Status.NOT_FOUND); - } - - LinesWithOffset lastOutputLines = lastOutputLinesOpt.orElseThrow(); - - return new GetTaskOutputReply(lastOutputLines.getLines(), lastOutputLines.getFirstLineOffset()); - } - - private static class PostOneUpReply { - - public final List tasks; - - public PostOneUpReply(List tasks) { - this.tasks = tasks; - } - } - - private static class PostCommitReply { - - public final JsonTask task; - - public PostCommitReply(JsonTask task) { - this.task = task; - } - } - - private static class UploadTarReply { - - public final JsonTask task; - - public UploadTarReply(JsonTask task) { - this.task = task; - } - } - - private static class GetQueueReply { - - public final List tasks; - public final List runners; - - public GetQueueReply(List tasks, List runners) { - this.tasks = tasks; - this.runners = runners; - } - } - - private static class GetTaskOutputReply { - - public final List output; - public final int indexOfFirstLine; - - public GetTaskOutputReply(List output, int indexOfFirstLine) { - this.output = output; - this.indexOfFirstLine = indexOfFirstLine; - } - } - - private static class GetTaskInfoReply { - - public final JsonTask task; - public final int position; - @Nullable - public final Long runningSince; - - public GetTaskInfoReply(JsonTask task, int position, @Nullable Long runningSince) { - this.task = task; - this.position = position; - this.runningSince = runningSince; - } - } + private static final String AUTHOR_NAME_ADMIN = "Admin"; + + private final CommitReadAccess commitReadAccess; + private final RepoReadAccess repoReadAccess; + private final Queue queue; + private final IDispatcher dispatcher; + + public QueueEndpoint( + CommitReadAccess commitReadAccess, + RepoReadAccess repoReadAccess, + Queue queue, + IDispatcher dispatcher) { + + this.commitReadAccess = commitReadAccess; + this.repoReadAccess = repoReadAccess; + this.queue = queue; + this.dispatcher = dispatcher; + } + + @GET + @Timed(histogram = true) + public GetQueueReply getQueue() { + List tasks = queue.getAllTasksInOrder(); + + Map> hashPerRepo = + tasks.stream() + // do + // repoId <- taskRepoId t + // hash <- getHash <$> getLeft (taskSource t) + // pure (repoId, hash) + .flatMap( + task -> + task + .getSource() + .getLeft() + .flatMap(cs -> task.getRepoId().map(rid -> new Pair<>(rid, cs.getHash()))) + .stream()) + .collect( + Collectors.groupingBy( + Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList()))); + + Map> commitPerHashPerRepo = + hashPerRepo.entrySet().stream() + .collect( + toMap( + Entry::getKey, + it -> + commitReadAccess.getCommits(it.getKey(), it.getValue()).stream() + .collect(toMap(Commit::getHash, commit -> commit)))); + + List jsonTasks = + tasks.stream() + .map( + task -> { + JsonSource source = + task.getSource() + .mapLeft( + it -> { + RepoId repoId = it.getRepoId(); + CommitHash hash = it.getHash(); + return commitPerHashPerRepo + .get(repoId) + .getOrDefault(hash, Commit.placeholder(repoId, hash)); + }) + .consume(JsonSource::fromCommit, JsonSource::fromTarSource); + return JsonTask.fromTask(task, source); + }) + .collect(Collectors.toList()); + + List worker = + dispatcher.getKnownRunners().stream() + .map(JsonRunner::fromKnownRunner) + .collect(Collectors.toList()); + + return new GetQueueReply(jsonTasks, worker); + } + + @DELETE + @Timed(histogram = true) + public void emptyQueue(@Auth Admin admin) throws NoSuchTaskException { + queue.deleteAllTasks(); + } + + @DELETE + @Path("{taskid}") + @Timed(histogram = true) + public void deleteTask(@Auth Admin admin, @PathParam("taskid") UUID taskUuid) + throws NoSuchTaskException { + + TaskId taskId = new TaskId(taskUuid); + queue.guardTaskExists(taskId); + queue.deleteTasks(List.of(taskId)); + } + + @PATCH + @Path("{taskid}") + @Timed(histogram = true) + public void patchTask(@Auth Admin admin, @PathParam("taskid") UUID taskUuid) + throws NoSuchTaskException { + + TaskId taskId = new TaskId(taskUuid); + queue.guardTaskExists(taskId); + queue.prioritizeTask(taskId, TaskPriority.MANUAL); + } + + @POST + @Path("commit/{repoid}/{hash}") + @Timed(histogram = true) + public PostCommitReply addCommit( + @Auth Admin admin, + @PathParam("repoid") UUID repoUuid, + @PathParam("hash") String commitHashString) + throws NoSuchRepoException { + RepoId repoId = new RepoId(repoUuid); + CommitHash commitHash = new CommitHash(commitHashString); + + repoReadAccess.guardRepoExists(repoId); + commitReadAccess.guardCommitExists(repoId, commitHash); + // If at any point between these checks and inserting the task the repo is deleted, JOOQ will + // throw a DataAccessException (I believe) and this endpoint will return a 500. But the chance + // of that happening is low enough that putting in the effort to perform the checks in an atomic + // way is not worth it. + + final Optional insertedTask = + queue.addCommit(AUTHOR_NAME_ADMIN, repoId, commitHash, TaskPriority.MANUAL); + + if (insertedTask.isEmpty()) { + throw new TaskAlreadyExistsException(commitHash, repoId); + } + + return new PostCommitReply(JsonTask.fromTask(insertedTask.get(), commitReadAccess)); + } + + @POST + @Path("commit/{repoid}/{hash}/one-up") + @Timed(histogram = true) + public PostOneUpReply PostOneUpReply( + @Auth Admin admin, + @PathParam("repoid") UUID repoUuid, + @PathParam("hash") String commitHashString) + throws NoSuchRepoException { + RepoId repoId = new RepoId(repoUuid); + CommitHash rootHash = new CommitHash(commitHashString); + + commitReadAccess.guardCommitExists(repoId, rootHash); + + List hashes = commitReadAccess.getDescendantCommits(repoId, rootHash); + + queue.addCommits(AUTHOR_NAME_ADMIN, repoId, hashes, TaskPriority.LISTENER); + + List tasks = + queue.getAllTasksInOrder().stream() + .filter(it -> it.getRepoId().map(repoId::equals).orElse(false)) + .filter(it -> it.getCommitHash().isPresent()) + .filter(it -> hashes.contains(it.getCommitHash().get())) + .map(task -> JsonTask.fromTask(task, commitReadAccess)) + .collect(Collectors.toList()); + + return new PostOneUpReply(tasks); + } + + @POST + @Path("upload/tar") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Timed(histogram = true) + public UploadTarReply uploadTar( + @Auth Admin admin, + @NotNull @FormDataParam("description") String description, + @Nullable @FormDataParam("repo_id") UUID repoUuid, + @NotNull @FormDataParam("file") InputStream inputStream, + @NotNull @FormDataParam("file") FormDataContentDisposition fileDisposition) + throws IOException { + Optional repoId = Optional.ofNullable(repoUuid).map(RepoId::new); + + final InputStream uncompressedInput = + fileDisposition.getFileName().endsWith(".tar.gz") + ? new GZIPInputStream(inputStream) + : inputStream; + + try (uncompressedInput) { + Task task = + queue.addTar( + AUTHOR_NAME_ADMIN, + TaskPriority.MANUAL, + repoId.orElse(null), + description, + uncompressedInput); + + return new UploadTarReply(JsonTask.fromTask(task, commitReadAccess)); + } catch (TaskCreationException e) { + Status status = e.isOurFault() ? Status.INTERNAL_SERVER_ERROR : Status.BAD_REQUEST; + throw new WebApplicationException(e.getMessage(), status); + } + } + + @Path("task/{taskid}") + @GET + @Timed(histogram = true) + public GetTaskInfoReply getTask(@PathParam("taskid") UUID taskId) { + int indexOfTask = 0; + Optional foundTask = Optional.empty(); + for (Task task : queue.getAllTasksInOrder()) { + if (task.getIdAsUuid().equals(taskId)) { + foundTask = Optional.of(task); + break; + } + indexOfTask++; + } + + if (foundTask.isEmpty()) { + throw new WebApplicationException(Status.NOT_FOUND); + } + + Long runningSince = + dispatcher.getKnownRunners().stream() + .filter(it -> it.getCurrentTask().isPresent()) + .filter(it -> it.getCurrentTask().get().getId().getId().equals(taskId)) + .findAny() + .flatMap(KnownRunner::getWorkingSince) + .map(Instant::getEpochSecond) + .orElse(null); + + return new GetTaskInfoReply( + JsonTask.fromTask(foundTask.get(), commitReadAccess), indexOfTask, runningSince); + } + + @Path("task/{taskid}/progress") + @GET + @Timed(histogram = true) + public GetTaskOutputReply getRunnerOutput(@PathParam("taskid") UUID taskId) { + Optional lastOutputLinesOpt = dispatcher.findLinesForTask(taskId); + + if (lastOutputLinesOpt.isEmpty()) { + throw new WebApplicationException(Status.NOT_FOUND); + } + + LinesWithOffset lastOutputLines = lastOutputLinesOpt.orElseThrow(); + + return new GetTaskOutputReply(lastOutputLines.getLines(), lastOutputLines.getFirstLineOffset()); + } + + private static class PostOneUpReply { + + public final List tasks; + + public PostOneUpReply(List tasks) { + this.tasks = tasks; + } + } + + private static class PostCommitReply { + + public final JsonTask task; + + public PostCommitReply(JsonTask task) { + this.task = task; + } + } + + private static class UploadTarReply { + + public final JsonTask task; + + public UploadTarReply(JsonTask task) { + this.task = task; + } + } + + private static class GetQueueReply { + + public final List tasks; + public final List runners; + + public GetQueueReply(List tasks, List runners) { + this.tasks = tasks; + this.runners = runners; + } + } + + private static class GetTaskOutputReply { + + public final List output; + public final int indexOfFirstLine; + + public GetTaskOutputReply(List output, int indexOfFirstLine) { + this.output = output; + this.indexOfFirstLine = indexOfFirstLine; + } + } + + private static class GetTaskInfoReply { + + public final JsonTask task; + public final int position; + @Nullable public final Long runningSince; + + public GetTaskInfoReply(JsonTask task, int position, @Nullable Long runningSince) { + this.task = task; + this.position = position; + this.runningSince = runningSince; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RecentRunsEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RecentRunsEndpoint.java index 7acd96420..b252eea79 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RecentRunsEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RecentRunsEndpoint.java @@ -29,119 +29,120 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -/** - * Endpoint for getting a list of the most recent runs. - */ +/** Endpoint for getting a list of the most recent runs. */ @Path("/recent/runs") @Produces(MediaType.APPLICATION_JSON) public class RecentRunsEndpoint { - private static final int DEFAULT_N = 10; - private static final int MIN_N = 1; - private static final int MAX_N = 100; - - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final RunCache runCache; - private final SignificantRunsCollector significantRunsCollector; - - public RecentRunsEndpoint(BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess, RunCache runCache, - SignificantRunsCollector significantRunsCollector) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.runCache = runCache; - this.significantRunsCollector = significantRunsCollector; - } - - @GET - @Timed(histogram = true) - public GetReply getRuns( - @QueryParam("n") @Nullable Integer nOptional, - @QueryParam("significant") @Nullable Boolean significantOptional - ) { - int n = (nOptional == null) ? DEFAULT_N : nOptional; - n = Math.max(MIN_N, Math.min(MAX_N, n)); - - boolean significant = (significantOptional != null) && significantOptional; - - final List runEntries; - if (significant) { - runEntries = significantRunsCollector.collectMostRecent(n).stream() - .map(run -> toJsonRunEntry(run.getRun(), run.getReasons())) - .collect(toList()); - } else { - List recentRunIds = benchmarkAccess.getRecentRunIds(0, n); - runEntries = runCache.getRunsInOrder(benchmarkAccess, recentRunIds).stream() - .map(run -> toJsonRunEntry(run, null)) - .collect(toList()); - } - - return new GetReply(runEntries); - } - - private JsonRunEntry toJsonRunEntry(Run run, @Nullable SignificanceReasons reasons) { - final Optional> significantDifferences; - final Optional> significantFailedDimensions; - - if (reasons == null) { - significantDifferences = Optional.empty(); - significantFailedDimensions = Optional.empty(); - } else { - Set dimensions = reasons.getDimensions(); - Map dimInfos = dimensionAccess.getDimensionInfoMap(dimensions); - - significantDifferences = Optional.of(reasons.getSignificantDifferences() - .stream() - .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, dimInfos)) - .collect(toList())); - - significantFailedDimensions = Optional.of(reasons.getSignificantFailedDimensions() - .stream() - .map(dimInfos::get) - .map(JsonDimension::fromDimensionInfo) - .collect(toList())); - } - - return new JsonRunEntry( - new JsonRunDescription( - run.getId().getId(), - run.getStartTime().getEpochSecond(), - JsonSuccess.fromRunResult(run.getResult()), - JsonSource.fromSource(run.getSource(), commitAccess) - ), - significantDifferences.orElse(null), - significantFailedDimensions.orElse(null) - ); - } - - private static class GetReply { - - public final List runs; - - public GetReply(List runs) { - this.runs = runs; - } - } - - private static class JsonRunEntry { - - public final JsonRunDescription run; - @Nullable - public final List significantDifferences; - @Nullable - public final List significantFailedDimensions; - - public JsonRunEntry(JsonRunDescription run, - @Nullable List significantDifferences, - @Nullable List significantFailedDimensions) { - - this.run = run; - this.significantDifferences = significantDifferences; - this.significantFailedDimensions = significantFailedDimensions; - } - } + private static final int DEFAULT_N = 10; + private static final int MIN_N = 1; + private static final int MAX_N = 100; + + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final RunCache runCache; + private final SignificantRunsCollector significantRunsCollector; + + public RecentRunsEndpoint( + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + RunCache runCache, + SignificantRunsCollector significantRunsCollector) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.runCache = runCache; + this.significantRunsCollector = significantRunsCollector; + } + + @GET + @Timed(histogram = true) + public GetReply getRuns( + @QueryParam("n") @Nullable Integer nOptional, + @QueryParam("significant") @Nullable Boolean significantOptional) { + int n = (nOptional == null) ? DEFAULT_N : nOptional; + n = Math.max(MIN_N, Math.min(MAX_N, n)); + + boolean significant = (significantOptional != null) && significantOptional; + + final List runEntries; + if (significant) { + runEntries = + significantRunsCollector.collectMostRecent(n).stream() + .map(run -> toJsonRunEntry(run.getRun(), run.getReasons())) + .collect(toList()); + } else { + List recentRunIds = benchmarkAccess.getRecentRunIds(0, n); + runEntries = + runCache.getRunsInOrder(benchmarkAccess, recentRunIds).stream() + .map(run -> toJsonRunEntry(run, null)) + .collect(toList()); + } + + return new GetReply(runEntries); + } + + private JsonRunEntry toJsonRunEntry(Run run, @Nullable SignificanceReasons reasons) { + final Optional> significantDifferences; + final Optional> significantFailedDimensions; + + if (reasons == null) { + significantDifferences = Optional.empty(); + significantFailedDimensions = Optional.empty(); + } else { + Set dimensions = reasons.getDimensions(); + Map dimInfos = dimensionAccess.getDimensionInfoMap(dimensions); + + significantDifferences = + Optional.of( + reasons.getSignificantDifferences().stream() + .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, dimInfos)) + .collect(toList())); + + significantFailedDimensions = + Optional.of( + reasons.getSignificantFailedDimensions().stream() + .map(dimInfos::get) + .map(JsonDimension::fromDimensionInfo) + .collect(toList())); + } + + return new JsonRunEntry( + new JsonRunDescription( + run.getId().getId(), + run.getStartTime().getEpochSecond(), + JsonSuccess.fromRunResult(run.getResult()), + JsonSource.fromSource(run.getSource(), commitAccess)), + significantDifferences.orElse(null), + significantFailedDimensions.orElse(null)); + } + + private static class GetReply { + + public final List runs; + + public GetReply(List runs) { + this.runs = runs; + } + } + + private static class JsonRunEntry { + + public final JsonRunDescription run; + @Nullable public final List significantDifferences; + @Nullable public final List significantFailedDimensions; + + public JsonRunEntry( + JsonRunDescription run, + @Nullable List significantDifferences, + @Nullable List significantFailedDimensions) { + + this.run = run; + this.significantDifferences = significantDifferences; + this.significantFailedDimensions = significantFailedDimensions; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RepoEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RepoEndpoint.java index f3eeb4c37..4fb284529 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RepoEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RepoEndpoint.java @@ -47,236 +47,241 @@ @Path("/repo") @Produces(MediaType.APPLICATION_JSON) public class RepoEndpoint { - // Most of the logic found here was copied pretty much directly from the old repo endpoint. - - private static final Logger LOGGER = LoggerFactory.getLogger(RepoEndpoint.class); - - private final DimensionReadAccess dimensionAccess; - private final RepoWriteAccess repoAccess; - private final AvailableDimensionsCache availableDimensionsCache; - private final Listener listener; - - public RepoEndpoint(DimensionReadAccess dimensionAccess, RepoWriteAccess repoAccess, - AvailableDimensionsCache availableDimensionsCache, Listener listener) { - - this.dimensionAccess = dimensionAccess; - this.repoAccess = repoAccess; - this.availableDimensionsCache = availableDimensionsCache; - this.listener = listener; - } - - private JsonRepo toJsonRepo(Repo repo) { - List branches = repoAccess.getAllBranches(repo.getId()).stream() - .map(JsonBranch::fromBranch) - .collect(toList()); - - Set dimensions = availableDimensionsCache - .getAvailableDimensionsFor(dimensionAccess, repo.getId()); - List jsonDimensions = dimensionAccess.getDimensionInfos(dimensions).stream() - .map(JsonDimension::fromDimensionInfo) - .collect(toList()); - - return new JsonRepo( - repo.getIdAsUuid(), - repo.getName(), - repo.getRemoteUrlAsString(), - branches, - jsonDimensions, - repo.getGithubInfo() - .map(GithubInfo::getCommentCutoff) - .map(Instant::getEpochSecond) - .orElse(null) - ); - } - - @POST - @Timed(histogram = true) - public PostReply post(@Auth Admin admin, @NotNull PostRequest request) - throws FailedToAddRepoException { - - RemoteUrl remoteUrl = new RemoteUrl(request.getRemoteUrl()); - Repo repo = repoAccess.addRepo(request.getName(), remoteUrl); - - try { - listener.synchronizeCommitsForRepo(repo); - return new PostReply(toJsonRepo(repo)); - } catch (SynchronizeCommitsException e) { - repoAccess.deleteRepo(repo.getId()); - throw new WebApplicationException("Repo could not be cloned, invalid remote url", - Status.BAD_REQUEST); - } - } - - private static class PostRequest { - - private final String name; - private final String remoteUrl; - - @JsonCreator - public PostRequest( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String remoteUrl - ) { - this.name = name; - this.remoteUrl = remoteUrl; - } - - public String getName() { - return name; - } - - public String getRemoteUrl() { - return remoteUrl; - } - } - - private static class PostReply { - - private final JsonRepo repo; - - public PostReply(JsonRepo repo) { - this.repo = repo; - } - - public JsonRepo getRepo() { - return repo; - } - } - - @GET - @Path("{repoid}") - @Timed(histogram = true) - public GetReply get(@PathParam("repoid") UUID repoUuid) throws NoSuchRepoException { - RepoId repoId = new RepoId(repoUuid); - Repo repo = repoAccess.getRepo(repoId); - List commands = repoAccess.getCommands(repoId) - .stream() - .map(command -> new JsonGithubCommand( - command.getPr(), - command.getComment(), - command.getState().getTextualRepresentation() - )) - .collect(toList()); - - return new GetReply(toJsonRepo(repo), commands); - } - - private static class GetReply { - - public final JsonRepo repo; - public final List githubCommands; - - public GetReply(JsonRepo repo, List githubCommands) { - this.repo = repo; - this.githubCommands = githubCommands; - } - } - - private static class JsonGithubCommand { - - public final long prNumber; - public final long commentId; - public final String status; - - public JsonGithubCommand(long prNumber, long commentId, String status) { - this.prNumber = prNumber; - this.commentId = commentId; - this.status = status; - } - } - - @PATCH - @Path("{repoid}") - @Timed(histogram = true) - public void patch( - @Auth Admin admin, - @PathParam("repoid") UUID repoUuid, - @NotNull PatchRequest request - ) throws NoSuchRepoException { - RepoId repoId = new RepoId(repoUuid); - - // Guards whether the repo exists (that's why it's so high up in the function) - Repo repo = repoAccess.getRepo(repoId); - - repoAccess.updateRepo( - repoId, - request.getName().orElse(null), - request.getRemoteUrl().map(RemoteUrl::new).orElse(null) - ); - - request.getTrackedBranches().ifPresent(trackedBranches -> { - Set trackedBranchNames = trackedBranches.stream() - .map(BranchName::fromName) - .collect(Collectors.toSet()); - repoAccess.setTrackedBranches(repoId, trackedBranchNames); - - try { - listener.synchronizeCommitsForRepo(repo); - } catch (SynchronizeCommitsException e) { - LOGGER.warn("Failed to update repo {} successfully", repoId); - } - }); - - request.getGithubToken().ifPresent(token -> { - String stripped = token.strip(); - if (stripped.equals("")) { - repoAccess.unsetGithubAuthToken(repoId); - } else { - repoAccess.setGithubAuthToken(repoId, stripped); - } - }); - } - - private static class PatchRequest { - - @Nullable - private final String name; - @Nullable - private final String remoteUrl; - @Nullable - private final List trackedBranches; - @Nullable - private final String githubToken; - - @JsonCreator - public PatchRequest(@Nullable String name, @Nullable String remoteUrl, - @Nullable List trackedBranches, @Nullable String githubToken) { - - this.name = name; - this.remoteUrl = remoteUrl; - this.trackedBranches = trackedBranches; - this.githubToken = githubToken; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public Optional getRemoteUrl() { - return Optional.ofNullable(remoteUrl); - } - - public Optional> getTrackedBranches() { - return Optional.ofNullable(trackedBranches); - } - - public Optional getGithubToken() { - return Optional.ofNullable(githubToken); - } - } - - @DELETE - @Path("{repoid}") - @Timed(histogram = true) - public void delete(@Auth Admin admin, @PathParam("repoid") UUID repoUuid) - throws NoSuchRepoException { - - RepoId repoId = new RepoId(repoUuid); - repoAccess.guardRepoExists(repoId); - - // Also deletes the repo from all tables in the db that have a foreign key on the repo table - // since all (relevant) foreign key restraints are marked as ON DELETE CASCADE. This includes - // the queue table. - repoAccess.deleteRepo(repoId); - } + // Most of the logic found here was copied pretty much directly from the old repo endpoint. + + private static final Logger LOGGER = LoggerFactory.getLogger(RepoEndpoint.class); + + private final DimensionReadAccess dimensionAccess; + private final RepoWriteAccess repoAccess; + private final AvailableDimensionsCache availableDimensionsCache; + private final Listener listener; + + public RepoEndpoint( + DimensionReadAccess dimensionAccess, + RepoWriteAccess repoAccess, + AvailableDimensionsCache availableDimensionsCache, + Listener listener) { + + this.dimensionAccess = dimensionAccess; + this.repoAccess = repoAccess; + this.availableDimensionsCache = availableDimensionsCache; + this.listener = listener; + } + + private JsonRepo toJsonRepo(Repo repo) { + List branches = + repoAccess.getAllBranches(repo.getId()).stream() + .map(JsonBranch::fromBranch) + .collect(toList()); + + Set dimensions = + availableDimensionsCache.getAvailableDimensionsFor(dimensionAccess, repo.getId()); + List jsonDimensions = + dimensionAccess.getDimensionInfos(dimensions).stream() + .map(JsonDimension::fromDimensionInfo) + .collect(toList()); + + return new JsonRepo( + repo.getIdAsUuid(), + repo.getName(), + repo.getRemoteUrlAsString(), + branches, + jsonDimensions, + repo.getGithubInfo() + .map(GithubInfo::getCommentCutoff) + .map(Instant::getEpochSecond) + .orElse(null)); + } + + @POST + @Timed(histogram = true) + public PostReply post(@Auth Admin admin, @NotNull PostRequest request) + throws FailedToAddRepoException { + + RemoteUrl remoteUrl = new RemoteUrl(request.getRemoteUrl()); + Repo repo = repoAccess.addRepo(request.getName(), remoteUrl); + + try { + listener.synchronizeCommitsForRepo(repo); + return new PostReply(toJsonRepo(repo)); + } catch (SynchronizeCommitsException e) { + repoAccess.deleteRepo(repo.getId()); + throw new WebApplicationException( + "Repo could not be cloned, invalid remote url", Status.BAD_REQUEST); + } + } + + private static class PostRequest { + + private final String name; + private final String remoteUrl; + + @JsonCreator + public PostRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String remoteUrl) { + this.name = name; + this.remoteUrl = remoteUrl; + } + + public String getName() { + return name; + } + + public String getRemoteUrl() { + return remoteUrl; + } + } + + private static class PostReply { + + private final JsonRepo repo; + + public PostReply(JsonRepo repo) { + this.repo = repo; + } + + public JsonRepo getRepo() { + return repo; + } + } + + @GET + @Path("{repoid}") + @Timed(histogram = true) + public GetReply get(@PathParam("repoid") UUID repoUuid) throws NoSuchRepoException { + RepoId repoId = new RepoId(repoUuid); + Repo repo = repoAccess.getRepo(repoId); + List commands = + repoAccess.getCommands(repoId).stream() + .map( + command -> + new JsonGithubCommand( + command.getPr(), + command.getComment(), + command.getState().getTextualRepresentation())) + .collect(toList()); + + return new GetReply(toJsonRepo(repo), commands); + } + + private static class GetReply { + + public final JsonRepo repo; + public final List githubCommands; + + public GetReply(JsonRepo repo, List githubCommands) { + this.repo = repo; + this.githubCommands = githubCommands; + } + } + + private static class JsonGithubCommand { + + public final long prNumber; + public final long commentId; + public final String status; + + public JsonGithubCommand(long prNumber, long commentId, String status) { + this.prNumber = prNumber; + this.commentId = commentId; + this.status = status; + } + } + + @PATCH + @Path("{repoid}") + @Timed(histogram = true) + public void patch( + @Auth Admin admin, @PathParam("repoid") UUID repoUuid, @NotNull PatchRequest request) + throws NoSuchRepoException { + RepoId repoId = new RepoId(repoUuid); + + // Guards whether the repo exists (that's why it's so high up in the function) + Repo repo = repoAccess.getRepo(repoId); + + repoAccess.updateRepo( + repoId, + request.getName().orElse(null), + request.getRemoteUrl().map(RemoteUrl::new).orElse(null)); + + request + .getTrackedBranches() + .ifPresent( + trackedBranches -> { + Set trackedBranchNames = + trackedBranches.stream().map(BranchName::fromName).collect(Collectors.toSet()); + repoAccess.setTrackedBranches(repoId, trackedBranchNames); + + try { + listener.synchronizeCommitsForRepo(repo); + } catch (SynchronizeCommitsException e) { + LOGGER.warn("Failed to update repo {} successfully", repoId); + } + }); + + request + .getGithubToken() + .ifPresent( + token -> { + String stripped = token.strip(); + if (stripped.equals("")) { + repoAccess.unsetGithubAuthToken(repoId); + } else { + repoAccess.setGithubAuthToken(repoId, stripped); + } + }); + } + + private static class PatchRequest { + + @Nullable private final String name; + @Nullable private final String remoteUrl; + @Nullable private final List trackedBranches; + @Nullable private final String githubToken; + + @JsonCreator + public PatchRequest( + @Nullable String name, + @Nullable String remoteUrl, + @Nullable List trackedBranches, + @Nullable String githubToken) { + + this.name = name; + this.remoteUrl = remoteUrl; + this.trackedBranches = trackedBranches; + this.githubToken = githubToken; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public Optional getRemoteUrl() { + return Optional.ofNullable(remoteUrl); + } + + public Optional> getTrackedBranches() { + return Optional.ofNullable(trackedBranches); + } + + public Optional getGithubToken() { + return Optional.ofNullable(githubToken); + } + } + + @DELETE + @Path("{repoid}") + @Timed(histogram = true) + public void delete(@Auth Admin admin, @PathParam("repoid") UUID repoUuid) + throws NoSuchRepoException { + + RepoId repoId = new RepoId(repoUuid); + repoAccess.guardRepoExists(repoId); + + // Also deletes the repo from all tables in the db that have a foreign key on the repo table + // since all (relevant) foreign key restraints are marked as ON DELETE CASCADE. This includes + // the queue table. + repoAccess.deleteRepo(repoId); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RunEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RunEndpoint.java index 2038aa64f..c4b0475ad 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RunEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/RunEndpoint.java @@ -49,155 +49,159 @@ @Produces(MediaType.APPLICATION_JSON) public class RunEndpoint { - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final DimensionReadAccess dimensionAccess; - private final RunCache runCache; - private final LatestRunCache latestRunCache; - private final RunComparator comparer; - private final SignificanceFactors significanceFactors; - private final SignificantRunsCollector significantRunsCollector; - - public RunEndpoint(BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - DimensionReadAccess dimensionAccess, RunCache runCache, LatestRunCache latestRunCache, - RunComparator comparer, SignificanceFactors significanceFactors, - SignificantRunsCollector significantRunsCollector) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.dimensionAccess = dimensionAccess; - this.runCache = runCache; - this.latestRunCache = latestRunCache; - this.comparer = comparer; - this.significanceFactors = significanceFactors; - this.significantRunsCollector = significantRunsCollector; - } - - private Optional getPrevRun(Run run) { - Optional left = run.getSource().getLeft(); - if (left.isEmpty()) { - return Optional.empty(); // Run doesn't come from a commit - } - CommitSource commitSource = left.get(); - - Iterator parentHashIt = commitAccess - .getParentHashes(commitSource.getRepoId(), commitSource.getHash()) - .iterator(); - if (parentHashIt.hasNext()) { - CommitHash parentHash = parentHashIt.next(); - return latestRunCache - .getLatestRun(benchmarkAccess, runCache, commitSource.getRepoId(), parentHash); - } else { - return Optional.empty(); // No unambiguous previous commit - } - } - - @GET - @Path("{runid}") - @Timed(histogram = true) - public GetReply get( - @PathParam("runid") UUID runUuid, - @QueryParam("all_values") @Nullable Boolean allValuesOptional, - @QueryParam("hash") @Nullable String hashString, - @QueryParam("diff_prev") @Nullable Boolean diffPrevOptional - ) throws NoSuchRunException { - boolean allValues = (allValuesOptional != null) && allValuesOptional; - boolean diffPrev = (diffPrevOptional != null) && diffPrevOptional; - - Run run = EndpointUtils.getRun(benchmarkAccess, runCache, latestRunCache, runUuid, hashString); - - // Obtain differences to previous run - final Optional> differences; - final Optional> significantDifferences; - final Optional> significantFailedDimensions; - if (diffPrev) { - Optional> prevRunDiffs = getPrevRun(run) - .map(it -> comparer.compare(it, run)) - .map(RunComparison::getDifferences); - - Optional significanceReasons = significantRunsCollector - .getSignificantRun(run) - .map(SignificantRun::getReasons); - - Set dimensions = Stream.concat( - prevRunDiffs.stream() - .flatMap(Collection::stream) - .map(DimensionDifference::getDimension), - significanceReasons.stream() - .map(SignificanceReasons::getDimensions) - .flatMap(Collection::stream) - ).collect(toSet()); - - Map infos = dimensionAccess.getDimensionInfoMap(dimensions); - - differences = prevRunDiffs - .map(diffs -> diffs.stream() - .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, infos)) - .collect(toList())); - - significantDifferences = significanceReasons - .map(reasons -> reasons.getSignificantDifferences() - .stream() - .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, infos)) - .collect(toList())); - - significantFailedDimensions = significanceReasons - .map(reasons -> reasons.getSignificantFailedDimensions() - .stream() - .map(infos::get) - .map(JsonDimension::fromDimensionInfo) - .collect(toList())); - } else { - differences = Optional.empty(); - significantDifferences = Optional.empty(); - significantFailedDimensions = Optional.empty(); - } - - return new GetReply( - EndpointUtils.fromRun(dimensionAccess, commitAccess, run, significanceFactors, allValues), - differences.orElse(null), - significantDifferences.orElse(null), - significantFailedDimensions.orElse(null) - ); - } - - private static class GetReply { - - public final JsonRun run; - @Nullable - public final List differences; - @Nullable - public final List significantDifferences; - @Nullable - public final List significantFailedDimensions; - - public GetReply(JsonRun run, @Nullable List differences, - @Nullable List significantDifferences, - @Nullable List significantFailedDimensions) { - - this.run = run; - this.differences = differences; - this.significantDifferences = significantDifferences; - this.significantFailedDimensions = significantFailedDimensions; - } - } - - @GET - @Path("{runid}/short") - @Timed(histogram = true) - public GetShortReply getShort(@PathParam("runid") UUID runUuid) throws NoSuchRunException { - RunId runId = new RunId(runUuid); - ShortRunDescription run = benchmarkAccess.getShortRunDescription(runId); - return new GetShortReply(JsonShortRunDescription.fromShortRunDescription(run)); - } - - private static class GetShortReply { - - public final JsonShortRunDescription run; - - public GetShortReply(JsonShortRunDescription run) { - this.run = run; - } - } + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final DimensionReadAccess dimensionAccess; + private final RunCache runCache; + private final LatestRunCache latestRunCache; + private final RunComparator comparer; + private final SignificanceFactors significanceFactors; + private final SignificantRunsCollector significantRunsCollector; + + public RunEndpoint( + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + DimensionReadAccess dimensionAccess, + RunCache runCache, + LatestRunCache latestRunCache, + RunComparator comparer, + SignificanceFactors significanceFactors, + SignificantRunsCollector significantRunsCollector) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.dimensionAccess = dimensionAccess; + this.runCache = runCache; + this.latestRunCache = latestRunCache; + this.comparer = comparer; + this.significanceFactors = significanceFactors; + this.significantRunsCollector = significantRunsCollector; + } + + private Optional getPrevRun(Run run) { + Optional left = run.getSource().getLeft(); + if (left.isEmpty()) { + return Optional.empty(); // Run doesn't come from a commit + } + CommitSource commitSource = left.get(); + + Iterator parentHashIt = + commitAccess.getParentHashes(commitSource.getRepoId(), commitSource.getHash()).iterator(); + if (parentHashIt.hasNext()) { + CommitHash parentHash = parentHashIt.next(); + return latestRunCache.getLatestRun( + benchmarkAccess, runCache, commitSource.getRepoId(), parentHash); + } else { + return Optional.empty(); // No unambiguous previous commit + } + } + + @GET + @Path("{runid}") + @Timed(histogram = true) + public GetReply get( + @PathParam("runid") UUID runUuid, + @QueryParam("all_values") @Nullable Boolean allValuesOptional, + @QueryParam("hash") @Nullable String hashString, + @QueryParam("diff_prev") @Nullable Boolean diffPrevOptional) + throws NoSuchRunException { + boolean allValues = (allValuesOptional != null) && allValuesOptional; + boolean diffPrev = (diffPrevOptional != null) && diffPrevOptional; + + Run run = EndpointUtils.getRun(benchmarkAccess, runCache, latestRunCache, runUuid, hashString); + + // Obtain differences to previous run + final Optional> differences; + final Optional> significantDifferences; + final Optional> significantFailedDimensions; + if (diffPrev) { + Optional> prevRunDiffs = + getPrevRun(run).map(it -> comparer.compare(it, run)).map(RunComparison::getDifferences); + + Optional significanceReasons = + significantRunsCollector.getSignificantRun(run).map(SignificantRun::getReasons); + + Set dimensions = + Stream.concat( + prevRunDiffs.stream() + .flatMap(Collection::stream) + .map(DimensionDifference::getDimension), + significanceReasons.stream() + .map(SignificanceReasons::getDimensions) + .flatMap(Collection::stream)) + .collect(toSet()); + + Map infos = dimensionAccess.getDimensionInfoMap(dimensions); + + differences = + prevRunDiffs.map( + diffs -> + diffs.stream() + .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, infos)) + .collect(toList())); + + significantDifferences = + significanceReasons.map( + reasons -> + reasons.getSignificantDifferences().stream() + .map(diff -> JsonDimensionDifference.fromDimensionDifference(diff, infos)) + .collect(toList())); + + significantFailedDimensions = + significanceReasons.map( + reasons -> + reasons.getSignificantFailedDimensions().stream() + .map(infos::get) + .map(JsonDimension::fromDimensionInfo) + .collect(toList())); + } else { + differences = Optional.empty(); + significantDifferences = Optional.empty(); + significantFailedDimensions = Optional.empty(); + } + + return new GetReply( + EndpointUtils.fromRun(dimensionAccess, commitAccess, run, significanceFactors, allValues), + differences.orElse(null), + significantDifferences.orElse(null), + significantFailedDimensions.orElse(null)); + } + + private static class GetReply { + + public final JsonRun run; + @Nullable public final List differences; + @Nullable public final List significantDifferences; + @Nullable public final List significantFailedDimensions; + + public GetReply( + JsonRun run, + @Nullable List differences, + @Nullable List significantDifferences, + @Nullable List significantFailedDimensions) { + + this.run = run; + this.differences = differences; + this.significantDifferences = significantDifferences; + this.significantFailedDimensions = significantFailedDimensions; + } + } + + @GET + @Path("{runid}/short") + @Timed(histogram = true) + public GetShortReply getShort(@PathParam("runid") UUID runUuid) throws NoSuchRunException { + RunId runId = new RunId(runUuid); + ShortRunDescription run = benchmarkAccess.getShortRunDescription(runId); + return new GetShortReply(JsonShortRunDescription.fromShortRunDescription(run)); + } + + private static class GetShortReply { + + public final JsonShortRunDescription run; + + public GetShortReply(JsonShortRunDescription run) { + this.run = run; + } + } } - diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/SearchEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/SearchEndpoint.java index 48b955cf3..45fa2f9d1 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/SearchEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/SearchEndpoint.java @@ -25,129 +25,124 @@ @Produces(MediaType.APPLICATION_JSON) public class SearchEndpoint { - private static final int SEARCH_LIMIT = 500; - - private final BenchmarkReadAccess benchmarkAccess; - private final CommitReadAccess commitAccess; - private final RepoReadAccess repoAccess; - - public SearchEndpoint(BenchmarkReadAccess benchmarkAccess, CommitReadAccess commitAccess, - RepoReadAccess repoAccess) { - - this.benchmarkAccess = benchmarkAccess; - this.commitAccess = commitAccess; - this.repoAccess = repoAccess; - } - - @GET - @Timed(histogram = true) - public SearchGetReply get( - @QueryParam("limit") @Nullable Integer maybeLimit, - @QueryParam("repo_id") @Nullable UUID repoUuid, - @QueryParam("query") @NotNull String query - ) { - Optional repoId = Optional.ofNullable(repoUuid).map(RepoId::new); - int limit = Optional.ofNullable(maybeLimit).orElse(SEARCH_LIMIT); - - List branches = repoAccess - .searchBranches(limit, repoId.orElse(null), query) - .stream() - .map(BranchInfo::new) - .collect(toList()); - - limit = Math.max(0, limit - branches.size()); - - List commits = commitAccess - .searchCommits(limit, repoId.orElse(null), query) - .stream() - .map(pair -> new CommitInfo(pair.getFirst(), pair.getSecond())) - .collect(toList()); - - limit = Math.max(0, limit - commits.size()); - - List runs = benchmarkAccess - .searchRuns(limit, repoId.orElse(null), query) - .stream() - .map(RunInfo::new) - .collect(toList()); - - return new SearchGetReply(commits, runs, branches); - } - - private static class SearchGetReply { - - public final List commits; - public final List runs; - public final List branches; - - public SearchGetReply(List commits, List runs, List branches) { - this.commits = commits; - this.runs = runs; - this.branches = branches; - } - } - - private static class CommitInfo { - - public final String repoId; - public final String hash; - public final String author; - public final long authorDate; - public final String committer; - public final long committerDate; - public final String summary; - public final boolean hasRun; - - public CommitInfo(Commit commit, boolean hasRun) { - - this.repoId = commit.getRepoId().getIdAsString(); - this.hash = commit.getHashAsString(); - this.author = commit.getAuthor(); - this.authorDate = commit.getAuthorDate().getEpochSecond(); - this.committer = commit.getCommitter(); - this.committerDate = commit.getCommitterDate().getEpochSecond(); - this.summary = commit.getSummary(); - this.hasRun = hasRun; - } - } - - private static class RunInfo { - - public final String id; - @Nullable - public final String repoId; - @Nullable - public final String commitSummary; - @Nullable - public final String tarDescription; - public final long startTime; - public final long stopTime; - - public RunInfo(SearchRunDescription run) { - - this.id = run.getId().getIdAsString(); - this.repoId = run.getRepoId().map(RepoId::getIdAsString).orElse(null); - this.commitSummary = run.getCommitSummary().orElse(null); - this.tarDescription = run.getTarDescription().orElse(null); - this.startTime = run.getStartTime().getEpochSecond(); - this.stopTime = run.getStopTime().getEpochSecond(); - } - } - - private static class BranchInfo { - - public final String repoId; - public final String name; - public final String commitHash; - public final String commitSummary; - public final boolean hasRun; - - public BranchInfo(SearchBranchDescription branch) { - this.repoId = branch.getRepoId().getIdAsString(); - this.name = branch.getName().getName(); - this.commitHash = branch.getCommitHash().getHash(); - this.commitSummary = branch.getCommitSummary(); - this.hasRun = branch.hasRun(); - } - } + private static final int SEARCH_LIMIT = 500; + + private final BenchmarkReadAccess benchmarkAccess; + private final CommitReadAccess commitAccess; + private final RepoReadAccess repoAccess; + + public SearchEndpoint( + BenchmarkReadAccess benchmarkAccess, + CommitReadAccess commitAccess, + RepoReadAccess repoAccess) { + + this.benchmarkAccess = benchmarkAccess; + this.commitAccess = commitAccess; + this.repoAccess = repoAccess; + } + + @GET + @Timed(histogram = true) + public SearchGetReply get( + @QueryParam("limit") @Nullable Integer maybeLimit, + @QueryParam("repo_id") @Nullable UUID repoUuid, + @QueryParam("query") @NotNull String query) { + Optional repoId = Optional.ofNullable(repoUuid).map(RepoId::new); + int limit = Optional.ofNullable(maybeLimit).orElse(SEARCH_LIMIT); + + List branches = + repoAccess.searchBranches(limit, repoId.orElse(null), query).stream() + .map(BranchInfo::new) + .collect(toList()); + + limit = Math.max(0, limit - branches.size()); + + List commits = + commitAccess.searchCommits(limit, repoId.orElse(null), query).stream() + .map(pair -> new CommitInfo(pair.getFirst(), pair.getSecond())) + .collect(toList()); + + limit = Math.max(0, limit - commits.size()); + + List runs = + benchmarkAccess.searchRuns(limit, repoId.orElse(null), query).stream() + .map(RunInfo::new) + .collect(toList()); + + return new SearchGetReply(commits, runs, branches); + } + + private static class SearchGetReply { + + public final List commits; + public final List runs; + public final List branches; + + public SearchGetReply(List commits, List runs, List branches) { + this.commits = commits; + this.runs = runs; + this.branches = branches; + } + } + + private static class CommitInfo { + + public final String repoId; + public final String hash; + public final String author; + public final long authorDate; + public final String committer; + public final long committerDate; + public final String summary; + public final boolean hasRun; + + public CommitInfo(Commit commit, boolean hasRun) { + + this.repoId = commit.getRepoId().getIdAsString(); + this.hash = commit.getHashAsString(); + this.author = commit.getAuthor(); + this.authorDate = commit.getAuthorDate().getEpochSecond(); + this.committer = commit.getCommitter(); + this.committerDate = commit.getCommitterDate().getEpochSecond(); + this.summary = commit.getSummary(); + this.hasRun = hasRun; + } + } + + private static class RunInfo { + + public final String id; + @Nullable public final String repoId; + @Nullable public final String commitSummary; + @Nullable public final String tarDescription; + public final long startTime; + public final long stopTime; + + public RunInfo(SearchRunDescription run) { + + this.id = run.getId().getIdAsString(); + this.repoId = run.getRepoId().map(RepoId::getIdAsString).orElse(null); + this.commitSummary = run.getCommitSummary().orElse(null); + this.tarDescription = run.getTarDescription().orElse(null); + this.startTime = run.getStartTime().getEpochSecond(); + this.stopTime = run.getStopTime().getEpochSecond(); + } + } + + private static class BranchInfo { + + public final String repoId; + public final String name; + public final String commitHash; + public final String commitSummary; + public final boolean hasRun; + + public BranchInfo(SearchBranchDescription branch) { + this.repoId = branch.getRepoId().getIdAsString(); + this.name = branch.getName().getName(); + this.commitHash = branch.getCommitHash().getHash(); + this.commitSummary = branch.getCommitSummary(); + this.hasRun = branch.hasRun(); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/TestTokenEndpoint.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/TestTokenEndpoint.java index e5f89e164..1c2898d73 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/TestTokenEndpoint.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/TestTokenEndpoint.java @@ -6,24 +6,20 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; -/** - * Endpoint for testing validity of access tokens. - */ +/** Endpoint for testing validity of access tokens. */ @Path("/test-token") public class TestTokenEndpoint { - public TestTokenEndpoint() { - } - - /** - * Checks whether an access token is valid for a given repo. - * - * @param admin auth guard - */ - @GET - @Timed(histogram = true) - public void get(@Auth Admin admin) { - // Being useful by doing nothing, when does that ever happen... - } + public TestTokenEndpoint() {} + /** + * Checks whether an access token is valid for a given repo. + * + * @param admin auth guard + */ + @GET + @Timed(histogram = true) + public void get(@Auth Admin admin) { + // Being useful by doing nothing, when does that ever happen... + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/ArgumentParseException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/ArgumentParseException.java index 03417e290..6044a31e0 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/ArgumentParseException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/ArgumentParseException.java @@ -1,11 +1,9 @@ package de.aaaaaaah.velcom.backend.restapi.endpoints.utils; -/** - * A query parameter could not be parsed correctly. - */ +/** A query parameter could not be parsed correctly. */ public class ArgumentParseException extends RuntimeException { - public ArgumentParseException(String message) { - super(message); - } + public ArgumentParseException(String message) { + super(message); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtils.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtils.java index 93bb884ab..a7f5a7537 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtils.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtils.java @@ -38,239 +38,259 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; -/** - * A utility class for helper functions specific to the REST API endpoints. - */ +/** A utility class for helper functions specific to the REST API endpoints. */ public class EndpointUtils { - private EndpointUtils() { - throw new UnsupportedOperationException(); - } + private EndpointUtils() { + throw new UnsupportedOperationException(); + } - /** - * Obtain a {@link Run} either by its ID or by a commit. - * - * @param benchmarkAccess a {@link BenchmarkReadAccess} - * @param runCache a {@link RunCache} - * @param latestRunCache a {@link LatestRunCache} - * @param id the run's ID, or the commit's repo id, if {@code hash} is specified - * @param hash the commit's hash, if the run should be obtained through a commit - * @return the run if it can be found - * @throws NoSuchRunException if the specified run does not exist - */ - public static Run getRun(BenchmarkReadAccess benchmarkAccess, RunCache runCache, - LatestRunCache latestRunCache, UUID id, @Nullable String hash) throws NoSuchRunException { + /** + * Obtain a {@link Run} either by its ID or by a commit. + * + * @param benchmarkAccess a {@link BenchmarkReadAccess} + * @param runCache a {@link RunCache} + * @param latestRunCache a {@link LatestRunCache} + * @param id the run's ID, or the commit's repo id, if {@code hash} is specified + * @param hash the commit's hash, if the run should be obtained through a commit + * @return the run if it can be found + * @throws NoSuchRunException if the specified run does not exist + */ + public static Run getRun( + BenchmarkReadAccess benchmarkAccess, + RunCache runCache, + LatestRunCache latestRunCache, + UUID id, + @Nullable String hash) + throws NoSuchRunException { - if (hash == null) { - RunId runId = new RunId(id); - return runCache.getRun(benchmarkAccess, runId); - } else { - RepoId repoId = new RepoId(id); - CommitHash commitHash = new CommitHash(hash); - return latestRunCache.getLatestRun(benchmarkAccess, runCache, repoId, commitHash) - .orElseThrow(() -> new NoSuchRunException(repoId, commitHash)); - } - } + if (hash == null) { + RunId runId = new RunId(id); + return runCache.getRun(benchmarkAccess, runId); + } else { + RepoId repoId = new RepoId(id); + CommitHash commitHash = new CommitHash(hash); + return latestRunCache + .getLatestRun(benchmarkAccess, runCache, repoId, commitHash) + .orElseThrow(() -> new NoSuchRunException(repoId, commitHash)); + } + } - /** - * Create a {@link JsonRun} from a {@link Run} and a few other variables. - * - * @param dimensionAccess a {@link DimensionReadAccess} - * @param commitAccess a {@link CommitReadAccess} - * @param run the run - * @param significanceFactors the current significance factors (required for stddev - * calculations) - * @param allValues whether the full lists of values should also be included for each - * measurement - * @return the created {@link JsonRun} - */ - public static JsonRun fromRun(DimensionReadAccess dimensionAccess, CommitReadAccess commitAccess, - Run run, SignificanceFactors significanceFactors, boolean allValues) { + /** + * Create a {@link JsonRun} from a {@link Run} and a few other variables. + * + * @param dimensionAccess a {@link DimensionReadAccess} + * @param commitAccess a {@link CommitReadAccess} + * @param run the run + * @param significanceFactors the current significance factors (required for stddev calculations) + * @param allValues whether the full lists of values should also be included for each measurement + * @return the created {@link JsonRun} + */ + public static JsonRun fromRun( + DimensionReadAccess dimensionAccess, + CommitReadAccess commitAccess, + Run run, + SignificanceFactors significanceFactors, + boolean allValues) { - JsonSource source = JsonSource.fromSource(run.getSource(), commitAccess); - Map dimensionInfos = dimensionAccess - .getDimensionInfoMap(run.getAllDimensionsUsed()); + JsonSource source = JsonSource.fromSource(run.getSource(), commitAccess); + Map dimensionInfos = + dimensionAccess.getDimensionInfoMap(run.getAllDimensionsUsed()); - return new JsonRun( - run.getId().getId(), - run.getAuthor(), - run.getRunnerName(), - run.getRunnerInfo(), - run.getStartTime().getEpochSecond(), - run.getStopTime().getEpochSecond(), - source, - run.getResult().consume( - JsonResult::fromRunError, - it -> JsonResult.fromMeasurements(it, dimensionInfos, significanceFactors, allValues) - ) - ); - } + return new JsonRun( + run.getId().getId(), + run.getAuthor(), + run.getRunnerName(), + run.getRunnerInfo(), + run.getStartTime().getEpochSecond(), + run.getStopTime().getEpochSecond(), + source, + run.getResult() + .consume( + JsonResult::fromRunError, + it -> + JsonResult.fromMeasurements( + it, dimensionInfos, significanceFactors, allValues))); + } - /** - * Parse a string of colon-separated arguments. The string is split into sections by double colons - * "::". The sections are split into elements by single colons ":". Each sections must contain at - * least two elements, the first of which is the section's name. - * - * @param args the string to parse - * @return the parsed sections and arguments - */ - public static List>> parseColonSeparatedArgs(String args) { - return Arrays.stream(args.split("::")) - .map(s -> { - String[] elements = s.split(":"); - if (elements.length == 0 || elements[0].equals("")) { - throw new ArgumentParseException("there needs to be at least one section"); - } else if (elements.length < 2) { - throw new ArgumentParseException("section \"" + s + "\" needs at least two elements"); - } + /** + * Parse a string of colon-separated arguments. The string is split into sections by double colons + * "::". The sections are split into elements by single colons ":". Each sections must contain at + * least two elements, the first of which is the section's name. + * + * @param args the string to parse + * @return the parsed sections and arguments + */ + public static List>> parseColonSeparatedArgs(String args) { + return Arrays.stream(args.split("::")) + .map( + s -> { + String[] elements = s.split(":"); + if (elements.length == 0 || elements[0].equals("")) { + throw new ArgumentParseException("there needs to be at least one section"); + } else if (elements.length < 2) { + throw new ArgumentParseException( + "section \"" + s + "\" needs at least two elements"); + } - String sectionName = elements[0]; - List sectionElements = Arrays.stream(elements).skip(1).collect(Collectors.toList()); - return new Pair<>(sectionName, sectionElements); - }) - .collect(Collectors.toList()); - } + String sectionName = elements[0]; + List sectionElements = + Arrays.stream(elements).skip(1).collect(Collectors.toList()); + return new Pair<>(sectionName, sectionElements); + }) + .collect(Collectors.toList()); + } - /** - * Parse a string of colon-separated arguments into dimensions. Each section represents multiple - * dimensions with the same benchmark. The section name is the benchmark and the section's other - * elements are the metrics. The same rules as with {@link #parseColonSeparatedArgs(String)} - * apply. - * - * @param args the string to parse - * @return the parsed dimensions - */ - public static Set parseDimensions(String args) { - return parseColonSeparatedArgs(args).stream() - .flatMap(pair -> pair.getSecond().stream() - .map(elem -> new Dimension(pair.getFirst(), elem))) - .collect(Collectors.toSet()); - } + /** + * Parse a string of colon-separated arguments into dimensions. Each section represents multiple + * dimensions with the same benchmark. The section name is the benchmark and the section's other + * elements are the metrics. The same rules as with {@link #parseColonSeparatedArgs(String)} + * apply. + * + * @param args the string to parse + * @return the parsed dimensions + */ + public static Set parseDimensions(String args) { + return parseColonSeparatedArgs(args).stream() + .flatMap( + pair -> pair.getSecond().stream().map(elem -> new Dimension(pair.getFirst(), elem))) + .collect(Collectors.toSet()); + } - /** - * Parse a string of colon-separated arguments into repos and their branches. Each section - * represents a repo with the repo name being the section's name. Each further element in the - * section is the name of a branch in the corresponding repo. The he same rules as with {@link - * #parseColonSeparatedArgs(String)} apply. - * - * @param args the string to parse - * @return the parsed repos and branch names - */ - public static Map> parseRepos(String args) { - return parseColonSeparatedArgs(args).stream() - .collect(Collectors.toMap( - pair -> new RepoId(UUID.fromString(pair.getFirst())), - pair -> pair.getSecond().stream().map(BranchName::fromName).collect(Collectors.toSet()) - )); - } + /** + * Parse a string of colon-separated arguments into repos and their branches. Each section + * represents a repo with the repo name being the section's name. Each further element in the + * section is the name of a branch in the corresponding repo. The he same rules as with {@link + * #parseColonSeparatedArgs(String)} apply. + * + * @param args the string to parse + * @return the parsed repos and branch names + */ + public static Map> parseRepos(String args) { + return parseColonSeparatedArgs(args).stream() + .collect( + Collectors.toMap( + pair -> new RepoId(UUID.fromString(pair.getFirst())), + pair -> + pair.getSecond().stream() + .map(BranchName::fromName) + .collect(Collectors.toSet()))); + } - /** - * Get the start and end time of an interval that is defined by zero, one or two of three possible - * values. If the start time and duration parameters are both null, the returned start time is - * null as this function will not guess a start time. Otherwise, the returned start and end time - * will never be null. - * - * @param startTimeEpoch the interval's start time in epoch time (seconds) - * @param endTimeEpoch the interval's end time in epoch time (seconds) - * @param durationInSeconds the interval's duration in seconds - * @return the interval's start and end time - * @throws InvalidQueryParamsException if all three parameters are not null - */ - public static Pair getStartAndEndTime(@Nullable Long startTimeEpoch, - @Nullable Long endTimeEpoch, @Nullable Integer durationInSeconds) { + /** + * Get the start and end time of an interval that is defined by zero, one or two of three possible + * values. If the start time and duration parameters are both null, the returned start time is + * null as this function will not guess a start time. Otherwise, the returned start and end time + * will never be null. + * + * @param startTimeEpoch the interval's start time in epoch time (seconds) + * @param endTimeEpoch the interval's end time in epoch time (seconds) + * @param durationInSeconds the interval's duration in seconds + * @return the interval's start and end time + * @throws InvalidQueryParamsException if all three parameters are not null + */ + public static Pair getStartAndEndTime( + @Nullable Long startTimeEpoch, + @Nullable Long endTimeEpoch, + @Nullable Integer durationInSeconds) { - // Parse startTime, endTime and duration - Instant startTime = startTimeEpoch == null ? null : Instant.ofEpochSecond(startTimeEpoch); - Instant endTime = endTimeEpoch == null ? null : Instant.ofEpochSecond(endTimeEpoch); - Duration duration = durationInSeconds == null ? null : Duration.ofSeconds(durationInSeconds); + // Parse startTime, endTime and duration + Instant startTime = startTimeEpoch == null ? null : Instant.ofEpochSecond(startTimeEpoch); + Instant endTime = endTimeEpoch == null ? null : Instant.ofEpochSecond(endTimeEpoch); + Duration duration = durationInSeconds == null ? null : Duration.ofSeconds(durationInSeconds); - // Relatively ugly start and end time logic, but I don't see any obvious way of making it more - // elegant and concise. - if (startTime != null && endTime != null && duration != null) { - throw new InvalidQueryParamsException( - "duration, start_time and end_time can't all be specified at the same time"); - } else if (duration != null) { - if (startTime != null) { - endTime = startTime.plus(duration); - } else if (endTime != null) { - startTime = endTime.minus(duration); - } else { - endTime = Instant.now(); - startTime = endTime.minus(duration); - } - } else { - if (endTime == null) { - endTime = Instant.now(); - } - } + // Relatively ugly start and end time logic, but I don't see any obvious way of making it more + // elegant and concise. + if (startTime != null && endTime != null && duration != null) { + throw new InvalidQueryParamsException( + "duration, start_time and end_time can't all be specified at the same time"); + } else if (duration != null) { + if (startTime != null) { + endTime = startTime.plus(duration); + } else if (endTime != null) { + startTime = endTime.minus(duration); + } else { + endTime = Instant.now(); + startTime = endTime.minus(duration); + } + } else { + if (endTime == null) { + endTime = Instant.now(); + } + } - return new Pair<>(startTime, endTime); - } + return new Pair<>(startTime, endTime); + } - /** - * @param commits the commits to sort - * @param hashes a map from hash to commit for exactly the commits in the - * commits list - * @return a mutable list containing a topological ordering of the input commits - */ - public static List topologicalSort(List commits, - Map hashes) { + /** + * @param commits the commits to sort + * @param hashes a map from hash to commit for exactly the commits in the + * commits list + * @return a mutable list containing a topological ordering of the input commits + */ + public static List topologicalSort( + List commits, Map hashes) { - // Based on Khan's Algorithm - // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + // Based on Khan's Algorithm + // https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm - List topologicallySorted = new ArrayList<>(); + List topologicallySorted = new ArrayList<>(); - Queue leaves = commits.stream() - // Only consider commits in our commits list. Throw away all children *not* in the list, as - // we are focusing on a small part of the graph and trying to sort only this part - // topologically. - // If we do not throw them away, we will find children outside the range, leading to too few - // found leaf nodes - .filter(it -> it.getChildHashes().stream().noneMatch(hashes::containsKey)) - .collect(toCollection(ArrayDeque::new)); + Queue leaves = + commits.stream() + // Only consider commits in our commits list. Throw away all children *not* in the list, + // as + // we are focusing on a small part of the graph and trying to sort only this part + // topologically. + // If we do not throw them away, we will find children outside the range, leading to too + // few + // found leaf nodes + .filter(it -> it.getChildHashes().stream().noneMatch(hashes::containsKey)) + .collect(toCollection(ArrayDeque::new)); - // We can not modify the actual children or remove edges. This is one of the pre-requisites for - // Khan's Algorithm though, as it deletes all explored edges starting with the leaves. - // Furthermore, not all children should be considered - only those that we know are in the - // commits list. All other children are outside the time range and irrelevant, as they should - // not appear in the graph. - // If we do not exclude those here, we will have too many children and won't always end up with - // *zero* leftover children, causing the Commit to not be recognized as a new leaf. - Map> parentChildMap = commits.stream() - .collect(toMap( - it -> it, - it -> it.getChildHashes().stream() - .filter(hashes::containsKey) - .collect(toCollection(HashSet::new)) - )); + // We can not modify the actual children or remove edges. This is one of the pre-requisites for + // Khan's Algorithm though, as it deletes all explored edges starting with the leaves. + // Furthermore, not all children should be considered - only those that we know are in the + // commits list. All other children are outside the time range and irrelevant, as they should + // not appear in the graph. + // If we do not exclude those here, we will have too many children and won't always end up with + // *zero* leftover children, causing the Commit to not be recognized as a new leaf. + Map> parentChildMap = + commits.stream() + .collect( + toMap( + it -> it, + it -> + it.getChildHashes().stream() + .filter(hashes::containsKey) + .collect(toCollection(HashSet::new)))); - while (!leaves.isEmpty()) { - FullCommit commit = leaves.poll(); - topologicallySorted.add(commit); + while (!leaves.isEmpty()) { + FullCommit commit = leaves.poll(); + topologicallySorted.add(commit); - for (CommitHash parentHash : commit.getParentHashes()) { - FullCommit parentCommit = hashes.get(parentHash); + for (CommitHash parentHash : commit.getParentHashes()) { + FullCommit parentCommit = hashes.get(parentHash); - // outside of our time bound (i.e. commits i.e. hashes) - if (parentCommit == null) { - continue; - } + // outside of our time bound (i.e. commits i.e. hashes) + if (parentCommit == null) { + continue; + } - Set existingchildren = parentChildMap.get(parentCommit); - existingchildren.remove(commit.getHash()); + Set existingchildren = parentChildMap.get(parentCommit); + existingchildren.remove(commit.getHash()); - // This commit has no other children in our time window -> it is now a leaf! - if (existingchildren.isEmpty()) { - leaves.add(parentCommit); - } - } - } + // This commit has no other children in our time window -> it is now a leaf! + if (existingchildren.isEmpty()) { + leaves.add(parentCommit); + } + } + } - // We appended to the end, so we now need to reverse it. Our leaves are the first entries - // currently, but they should be the last. - Collections.reverse(topologicallySorted); + // We appended to the end, so we now need to reverse it. Our leaves are the first entries + // currently, but they should be the last. + Collections.reverse(topologicallySorted); - return topologicallySorted; - } + return topologicallySorted; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/ArgumentParseExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/ArgumentParseExceptionMapper.java index 3c0ac7c4d..32d662029 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/ArgumentParseExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/ArgumentParseExceptionMapper.java @@ -5,29 +5,24 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link ArgumentParseException}s to BAD_REQUEST. - */ +/** An {@link ExceptionMapper} that transforms {@link ArgumentParseException}s to BAD_REQUEST. */ public class ArgumentParseExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(ArgumentParseException exception) { - return Response - .status(Status.BAD_REQUEST) - .entity(new Info(exception.getMessage())) - .build(); - } + @Override + public Response toResponse(ArgumentParseException exception) { + return Response.status(Status.BAD_REQUEST).entity(new Info(exception.getMessage())).build(); + } - private static class Info { + private static class Info { - private final String message; + private final String message; - public Info(String message) { - this.message = message; - } + public Info(String message) { + this.message = message; + } - public String getMessage() { - return message; - } - } + public String getMessage() { + return message; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsException.java index 1f53a0926..c8f8b41ac 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsException.java @@ -1,11 +1,9 @@ package de.aaaaaaah.velcom.backend.restapi.exception; -/** - * An exception thrown when an endpoint's query parameters are invalid. - */ +/** An exception thrown when an endpoint's query parameters are invalid. */ public class InvalidQueryParamsException extends RuntimeException { - public InvalidQueryParamsException(String message) { - super(message); - } + public InvalidQueryParamsException(String message) { + super(message); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsExceptionMapper.java index bd815eb46..faa95eb20 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/InvalidQueryParamsExceptionMapper.java @@ -7,27 +7,24 @@ /** * An {@link ExceptionMapper} that transforms {@link InvalidQueryParamsException}s to BAD_REQUEST. */ -public class InvalidQueryParamsExceptionMapper implements - ExceptionMapper { +public class InvalidQueryParamsExceptionMapper + implements ExceptionMapper { - @Override - public Response toResponse(InvalidQueryParamsException exception) { - return Response - .status(Status.BAD_REQUEST) - .entity(new Info(exception.getMessage())) - .build(); - } + @Override + public Response toResponse(InvalidQueryParamsException exception) { + return Response.status(Status.BAD_REQUEST).entity(new Info(exception.getMessage())).build(); + } - private static class Info { + private static class Info { - private final String message; + private final String message; - public Info(String message) { - this.message = message; - } + public Info(String message) { + this.message = message; + } - public String getMessage() { - return message; - } - } + public String getMessage() { + return message; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchCommitExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchCommitExceptionMapper.java index 0f271db48..03af2dff2 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchCommitExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchCommitExceptionMapper.java @@ -6,45 +6,42 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link NoSuchCommitException}s to NOT_FOUND. - */ +/** An {@link ExceptionMapper} that transforms {@link NoSuchCommitException}s to NOT_FOUND. */ public class NoSuchCommitExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(NoSuchCommitException exception) { - return Response - .status(Status.NOT_FOUND) - .entity(new Info( - "could not find commit", - exception.getRepoId().getId(), - exception.getCommitHash().getHash() - )) - .build(); - } - - private static class Info { - - private final String message; - private final UUID repoId; - private final String hash; - - public Info(String message, UUID repoId, String hash) { - this.message = message; - this.repoId = repoId; - this.hash = hash; - } - - public String getMessage() { - return message; - } - - public UUID getRepoId() { - return repoId; - } - - public String getHash() { - return hash; - } - } + @Override + public Response toResponse(NoSuchCommitException exception) { + return Response.status(Status.NOT_FOUND) + .entity( + new Info( + "could not find commit", + exception.getRepoId().getId(), + exception.getCommitHash().getHash())) + .build(); + } + + private static class Info { + + private final String message; + private final UUID repoId; + private final String hash; + + public Info(String message, UUID repoId, String hash) { + this.message = message; + this.repoId = repoId; + this.hash = hash; + } + + public String getMessage() { + return message; + } + + public UUID getRepoId() { + return repoId; + } + + public String getHash() { + return hash; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchDimensionExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchDimensionExceptionMapper.java index 9394a4904..2c06d559b 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchDimensionExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchDimensionExceptionMapper.java @@ -5,45 +5,42 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link NoSuchDimensionException}s to NOT_FOUND. - */ +/** An {@link ExceptionMapper} that transforms {@link NoSuchDimensionException}s to NOT_FOUND. */ public class NoSuchDimensionExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(NoSuchDimensionException exception) { - return Response - .status(Status.NOT_FOUND) - .entity(new Info( - "could not find dimension", - exception.getInvalidDimension().getBenchmark(), - exception.getInvalidDimension().getMetric() - )) - .build(); - } - - private static class Info { - - private final String message; - private final String benchmark; - private final String metric; - - public Info(String message, String benchmark, String metric) { - this.message = message; - this.benchmark = benchmark; - this.metric = metric; - } - - public String getMessage() { - return message; - } - - public String getBenchmark() { - return benchmark; - } - - public String getMetric() { - return metric; - } - } + @Override + public Response toResponse(NoSuchDimensionException exception) { + return Response.status(Status.NOT_FOUND) + .entity( + new Info( + "could not find dimension", + exception.getInvalidDimension().getBenchmark(), + exception.getInvalidDimension().getMetric())) + .build(); + } + + private static class Info { + + private final String message; + private final String benchmark; + private final String metric; + + public Info(String message, String benchmark, String metric) { + this.message = message; + this.benchmark = benchmark; + this.metric = metric; + } + + public String getMessage() { + return message; + } + + public String getBenchmark() { + return benchmark; + } + + public String getMetric() { + return metric; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRepoExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRepoExceptionMapper.java index 0bd2a8bb9..fb294ecf5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRepoExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRepoExceptionMapper.java @@ -6,38 +6,32 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link NoSuchRepoException}s to NOT_FOUND. - */ +/** An {@link ExceptionMapper} that transforms {@link NoSuchRepoException}s to NOT_FOUND. */ public class NoSuchRepoExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(NoSuchRepoException exception) { - return Response - .status(Status.NOT_FOUND) - .entity(new Info( - "could not find repo", - exception.getInvalidId().getId() - )) - .build(); - } - - private static class Info { - - private final String message; - private final UUID id; - - public Info(String message, UUID id) { - this.message = message; - this.id = id; - } - - public String getMessage() { - return message; - } - - public UUID getId() { - return id; - } - } + @Override + public Response toResponse(NoSuchRepoException exception) { + return Response.status(Status.NOT_FOUND) + .entity(new Info("could not find repo", exception.getInvalidId().getId())) + .build(); + } + + private static class Info { + + private final String message; + private final UUID id; + + public Info(String message, UUID id) { + this.message = message; + this.id = id; + } + + public String getMessage() { + return message; + } + + public UUID getId() { + return id; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRunExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRunExceptionMapper.java index 8a87b08b4..dc85d8b00 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRunExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchRunExceptionMapper.java @@ -12,42 +12,36 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link NoSuchRunException}s to NOT_FOUND. - */ +/** An {@link ExceptionMapper} that transforms {@link NoSuchRunException}s to NOT_FOUND. */ public class NoSuchRunExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(NoSuchRunException exception) { - Either> source = exception.getInvalidSource(); - return Response - .status(Status.NOT_FOUND) - .entity(new Info( - "could not find run", - source.getLeft().map(RunId::getId).orElse(null), - source.getRight().map(Pair::getFirst).map(RepoId::getId).orElse(null), - source.getRight().map(Pair::getSecond).map(CommitHash::getHash).orElse(null) - )) - .build(); - } + @Override + public Response toResponse(NoSuchRunException exception) { + Either> source = exception.getInvalidSource(); + return Response.status(Status.NOT_FOUND) + .entity( + new Info( + "could not find run", + source.getLeft().map(RunId::getId).orElse(null), + source.getRight().map(Pair::getFirst).map(RepoId::getId).orElse(null), + source.getRight().map(Pair::getSecond).map(CommitHash::getHash).orElse(null))) + .build(); + } - private static class Info { + private static class Info { - public final String message; - @Nullable - public final UUID id; - @Nullable - public final UUID repoId; - @Nullable - public final String commitHash; + public final String message; + @Nullable public final UUID id; + @Nullable public final UUID repoId; + @Nullable public final String commitHash; - public Info(String message, @Nullable UUID id, @Nullable UUID repoId, - @Nullable String commitHash) { + public Info( + String message, @Nullable UUID id, @Nullable UUID repoId, @Nullable String commitHash) { - this.message = message; - this.id = id; - this.repoId = repoId; - this.commitHash = commitHash; - } - } + this.message = message; + this.id = id; + this.repoId = repoId; + this.commitHash = commitHash; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchTaskExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchTaskExceptionMapper.java index 006fd40d7..04663e0c6 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchTaskExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/NoSuchTaskExceptionMapper.java @@ -6,38 +6,32 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link NoSuchTaskException}s to NOT_FOUND. - */ +/** An {@link ExceptionMapper} that transforms {@link NoSuchTaskException}s to NOT_FOUND. */ public class NoSuchTaskExceptionMapper implements ExceptionMapper { - @Override - public Response toResponse(NoSuchTaskException exception) { - return Response - .status(Status.NOT_FOUND) - .entity(new Info( - "could not find task", - exception.getInvalidId().getId() - )) - .build(); - } - - private static class Info { - - private final String message; - private final UUID id; - - public Info(String message, UUID id) { - this.message = message; - this.id = id; - } - - public String getMessage() { - return message; - } - - public UUID getId() { - return id; - } - } + @Override + public Response toResponse(NoSuchTaskException exception) { + return Response.status(Status.NOT_FOUND) + .entity(new Info("could not find task", exception.getInvalidId().getId())) + .build(); + } + + private static class Info { + + private final String message; + private final UUID id; + + public Info(String message, UUID id) { + this.message = message; + this.id = id; + } + + public String getMessage() { + return message; + } + + public UUID getId() { + return id; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsException.java index 762230729..78976b830 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsException.java @@ -3,25 +3,23 @@ import de.aaaaaaah.velcom.backend.access.committaccess.entities.CommitHash; import de.aaaaaaah.velcom.backend.access.repoaccess.entities.RepoId; -/** - * Thrown when a task already exists in the queue but is added again. - */ +/** Thrown when a task already exists in the queue but is added again. */ public class TaskAlreadyExistsException extends RuntimeException { - private final CommitHash hash; - private final RepoId repoId; + private final CommitHash hash; + private final RepoId repoId; - public TaskAlreadyExistsException(CommitHash hash, RepoId repoId) { - super("task already exists"); - this.hash = hash; - this.repoId = repoId; - } + public TaskAlreadyExistsException(CommitHash hash, RepoId repoId) { + super("task already exists"); + this.hash = hash; + this.repoId = repoId; + } - public CommitHash getHash() { - return hash; - } + public CommitHash getHash() { + return hash; + } - public RepoId getRepoId() { - return repoId; - } + public RepoId getRepoId() { + return repoId; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsExceptionMapper.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsExceptionMapper.java index f075b1484..86ca2b5cc 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsExceptionMapper.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/exception/TaskAlreadyExistsExceptionMapper.java @@ -5,46 +5,43 @@ import javax.ws.rs.core.Response.Status; import javax.ws.rs.ext.ExceptionMapper; -/** - * An {@link ExceptionMapper} that transforms {@link TaskAlreadyExistsException}s to CONFLICT. - */ -public class TaskAlreadyExistsExceptionMapper implements - ExceptionMapper { - - @Override - public Response toResponse(TaskAlreadyExistsException exception) { - return Response - .status(Status.CONFLICT) - .entity(new Info( - "that task already exists in the queue", - exception.getRepoId().getId(), - exception.getHash().getHash() - )) - .build(); - } - - private static class Info { - - private final String message; - private final UUID repoId; - private final String hash; - - public Info(String message, UUID repoId, String hash) { - this.message = message; - this.repoId = repoId; - this.hash = hash; - } - - public String getMessage() { - return message; - } - - public UUID getRepoId() { - return repoId; - } - - public String getHash() { - return hash; - } - } +/** An {@link ExceptionMapper} that transforms {@link TaskAlreadyExistsException}s to CONFLICT. */ +public class TaskAlreadyExistsExceptionMapper + implements ExceptionMapper { + + @Override + public Response toResponse(TaskAlreadyExistsException exception) { + return Response.status(Status.CONFLICT) + .entity( + new Info( + "that task already exists in the queue", + exception.getRepoId().getId(), + exception.getHash().getHash())) + .build(); + } + + private static class Info { + + private final String message; + private final UUID repoId; + private final String hash; + + public Info(String message, UUID repoId, String hash) { + this.message = message; + this.repoId = repoId; + this.hash = hash; + } + + public String getMessage() { + return message; + } + + public UUID getRepoId() { + return repoId; + } + + public String getHash() { + return hash; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonBranch.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonBranch.java index b2ae9bb4a..16f772faf 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonBranch.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonBranch.java @@ -4,33 +4,30 @@ public class JsonBranch { - private final String name; - private final boolean tracked; - private final String latest_commit; - - public JsonBranch(String name, boolean tracked, String latest_commit) { - this.name = name; - this.tracked = tracked; - this.latest_commit = latest_commit; - } - - public static JsonBranch fromBranch(Branch branch) { - return new JsonBranch( - branch.getName().getName(), - branch.isTracked(), - branch.getLatestCommitHash().getHash() - ); - } - - public String getName() { - return name; - } - - public boolean isTracked() { - return tracked; - } - - public String getLatest_commit() { - return latest_commit; - } + private final String name; + private final boolean tracked; + private final String latest_commit; + + public JsonBranch(String name, boolean tracked, String latest_commit) { + this.name = name; + this.tracked = tracked; + this.latest_commit = latest_commit; + } + + public static JsonBranch fromBranch(Branch branch) { + return new JsonBranch( + branch.getName().getName(), branch.isTracked(), branch.getLatestCommitHash().getHash()); + } + + public String getName() { + return name; + } + + public boolean isTracked() { + return tracked; + } + + public String getLatest_commit() { + return latest_commit; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommit.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommit.java index de8bbc1e5..8db1e26b5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommit.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommit.java @@ -6,98 +6,107 @@ public class JsonCommit { - private final UUID repoId; - private final String hash; - private final boolean tracked; - private final List trackedParents; - private final List untrackedParents; - private final List trackedChildren; - private final List untrackedChildren; - private final String author; - private final long authorDate; - private final String committer; - private final long committerDate; - private final String summary; - @Nullable - private final String message; - private final List runs; - - public JsonCommit(UUID repoId, String hash, boolean tracked, - List trackedParents, List untrackedParents, - List trackedChildren, List untrackedChildren, - String author, long authorDate, String committer, long committerDate, String summary, - @Nullable String message, List runs) { - - this.repoId = repoId; - this.hash = hash; - this.tracked = tracked; - this.trackedParents = trackedParents; - this.untrackedParents = untrackedParents; - this.trackedChildren = trackedChildren; - this.untrackedChildren = untrackedChildren; - this.author = author; - this.authorDate = authorDate; - this.committer = committer; - this.committerDate = committerDate; - this.summary = summary; - this.message = message; - this.runs = runs; - } - - public UUID getRepoId() { - return repoId; - } - - public String getHash() { - return hash; - } - - public boolean isTracked() { - return tracked; - } - - public List getTrackedParents() { - return trackedParents; - } - - public List getUntrackedParents() { - return untrackedParents; - } - - public List getTrackedChildren() { - return trackedChildren; - } - - public List getUntrackedChildren() { - return untrackedChildren; - } - - public String getAuthor() { - return author; - } - - public long getAuthorDate() { - return authorDate; - } - - public String getCommitter() { - return committer; - } - - public long getCommitterDate() { - return committerDate; - } - - public String getSummary() { - return summary; - } - - @Nullable - public String getMessage() { - return message; - } - - public List getRuns() { - return runs; - } + private final UUID repoId; + private final String hash; + private final boolean tracked; + private final List trackedParents; + private final List untrackedParents; + private final List trackedChildren; + private final List untrackedChildren; + private final String author; + private final long authorDate; + private final String committer; + private final long committerDate; + private final String summary; + @Nullable private final String message; + private final List runs; + + public JsonCommit( + UUID repoId, + String hash, + boolean tracked, + List trackedParents, + List untrackedParents, + List trackedChildren, + List untrackedChildren, + String author, + long authorDate, + String committer, + long committerDate, + String summary, + @Nullable String message, + List runs) { + + this.repoId = repoId; + this.hash = hash; + this.tracked = tracked; + this.trackedParents = trackedParents; + this.untrackedParents = untrackedParents; + this.trackedChildren = trackedChildren; + this.untrackedChildren = untrackedChildren; + this.author = author; + this.authorDate = authorDate; + this.committer = committer; + this.committerDate = committerDate; + this.summary = summary; + this.message = message; + this.runs = runs; + } + + public UUID getRepoId() { + return repoId; + } + + public String getHash() { + return hash; + } + + public boolean isTracked() { + return tracked; + } + + public List getTrackedParents() { + return trackedParents; + } + + public List getUntrackedParents() { + return untrackedParents; + } + + public List getTrackedChildren() { + return trackedChildren; + } + + public List getUntrackedChildren() { + return untrackedChildren; + } + + public String getAuthor() { + return author; + } + + public long getAuthorDate() { + return authorDate; + } + + public String getCommitter() { + return committer; + } + + public long getCommitterDate() { + return committerDate; + } + + public String getSummary() { + return summary; + } + + @Nullable + public String getMessage() { + return message; + } + + public List getRuns() { + return runs; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescription.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescription.java index c757381b9..da7719c38 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescription.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescription.java @@ -5,55 +5,54 @@ public class JsonCommitDescription { - private final UUID repoId; - private final String hash; - private final String author; - private final long authorDate; - private final String summary; - - public JsonCommitDescription(UUID repoId, String hash, String author, long authorDate, - String summary) { - - this.repoId = repoId; - this.hash = hash; - this.author = author; - this.authorDate = authorDate; - this.summary = summary; - } - - /** - * Utility function to create a {@link JsonCommitDescription} directly from a {@link Commit}. - * - * @param commit the commit to use - * @return a new description created from the commit - */ - public static JsonCommitDescription fromCommit(Commit commit) { - return new JsonCommitDescription( - commit.getRepoId().getId(), - commit.getHash().getHash(), - commit.getAuthor(), - commit.getAuthorDate().getEpochSecond(), - commit.getSummary() - ); - } - - public UUID getRepoId() { - return repoId; - } - - public String getHash() { - return hash; - } - - public String getAuthor() { - return author; - } - - public long getAuthorDate() { - return authorDate; - } - - public String getSummary() { - return summary; - } + private final UUID repoId; + private final String hash; + private final String author; + private final long authorDate; + private final String summary; + + public JsonCommitDescription( + UUID repoId, String hash, String author, long authorDate, String summary) { + + this.repoId = repoId; + this.hash = hash; + this.author = author; + this.authorDate = authorDate; + this.summary = summary; + } + + /** + * Utility function to create a {@link JsonCommitDescription} directly from a {@link Commit}. + * + * @param commit the commit to use + * @return a new description created from the commit + */ + public static JsonCommitDescription fromCommit(Commit commit) { + return new JsonCommitDescription( + commit.getRepoId().getId(), + commit.getHash().getHash(), + commit.getAuthor(), + commit.getAuthorDate().getEpochSecond(), + commit.getSummary()); + } + + public UUID getRepoId() { + return repoId; + } + + public String getHash() { + return hash; + } + + public String getAuthor() { + return author; + } + + public long getAuthorDate() { + return authorDate; + } + + public String getSummary() { + return summary; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimension.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimension.java index e994e82dc..1a9efa127 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimension.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimension.java @@ -5,48 +5,47 @@ public class JsonDimension { - private final String benchmark; - private final String metric; - private final String unit; - private final String interpretation; - - public JsonDimension(String benchmark, String metric, String unit, - Interpretation interpretation) { - - this.benchmark = benchmark; - this.metric = metric; - this.unit = unit; - this.interpretation = interpretation.getTextualRepresentation(); - } - - /** - * Create a {@link JsonDimension} from a {@link DimensionInfo}. - * - * @param dimensionInfo the {@link DimensionInfo} to use - * @return the newly created {@link JsonDimension} - */ - public static JsonDimension fromDimensionInfo(DimensionInfo dimensionInfo) { - return new JsonDimension( - dimensionInfo.getDimension().getBenchmark(), - dimensionInfo.getDimension().getMetric(), - dimensionInfo.getUnit().getName(), - dimensionInfo.getInterpretation() - ); - } - - public String getBenchmark() { - return benchmark; - } - - public String getMetric() { - return metric; - } - - public String getUnit() { - return unit; - } - - public String getInterpretation() { - return interpretation; - } + private final String benchmark; + private final String metric; + private final String unit; + private final String interpretation; + + public JsonDimension( + String benchmark, String metric, String unit, Interpretation interpretation) { + + this.benchmark = benchmark; + this.metric = metric; + this.unit = unit; + this.interpretation = interpretation.getTextualRepresentation(); + } + + /** + * Create a {@link JsonDimension} from a {@link DimensionInfo}. + * + * @param dimensionInfo the {@link DimensionInfo} to use + * @return the newly created {@link JsonDimension} + */ + public static JsonDimension fromDimensionInfo(DimensionInfo dimensionInfo) { + return new JsonDimension( + dimensionInfo.getDimension().getBenchmark(), + dimensionInfo.getDimension().getMetric(), + dimensionInfo.getUnit().getName(), + dimensionInfo.getInterpretation()); + } + + public String getBenchmark() { + return benchmark; + } + + public String getMetric() { + return metric; + } + + public String getUnit() { + return unit; + } + + public String getInterpretation() { + return interpretation; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionDifference.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionDifference.java index 615d2de57..fa31c0847 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionDifference.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionDifference.java @@ -12,69 +12,70 @@ public class JsonDimensionDifference { - private final JsonDimension dimension; - private final UUID oldRunId; - private final double diff; - @Nullable - private final Double reldiff; - @Nullable - private final Double stddevDiff; + private final JsonDimension dimension; + private final UUID oldRunId; + private final double diff; + @Nullable private final Double reldiff; + @Nullable private final Double stddevDiff; - public JsonDimensionDifference(JsonDimension dimension, UUID oldRunId, double diff, - @Nullable Double reldiff, @Nullable Double stddevDiff) { + public JsonDimensionDifference( + JsonDimension dimension, + UUID oldRunId, + double diff, + @Nullable Double reldiff, + @Nullable Double stddevDiff) { - this.dimension = dimension; - this.oldRunId = oldRunId; - this.diff = diff; - this.reldiff = reldiff; - this.stddevDiff = stddevDiff; - } + this.dimension = dimension; + this.oldRunId = oldRunId; + this.diff = diff; + this.reldiff = reldiff; + this.stddevDiff = stddevDiff; + } - /** - * Create a {@link JsonDimensionDifference} from a {@link DimensionDifference}. - * - * @param difference the {@link DimensionDifference} to use - * @return the newly created {@link JsonDimensionDifference} - */ - public static JsonDimensionDifference fromDimensionDifference(DimensionDifference difference, - Map dimensionInfos) { + /** + * Create a {@link JsonDimensionDifference} from a {@link DimensionDifference}. + * + * @param difference the {@link DimensionDifference} to use + * @return the newly created {@link JsonDimensionDifference} + */ + public static JsonDimensionDifference fromDimensionDifference( + DimensionDifference difference, Map dimensionInfos) { - return new JsonDimensionDifference( - JsonDimension.fromDimensionInfo(dimensionInfos.get(difference.getDimension())), - difference.getOldRunId().getId(), - difference.getDiff(), - difference.getReldiff().orElse(null), - difference.getStddevDiff().orElse(null) - ); - } + return new JsonDimensionDifference( + JsonDimension.fromDimensionInfo(dimensionInfos.get(difference.getDimension())), + difference.getOldRunId().getId(), + difference.getDiff(), + difference.getReldiff().orElse(null), + difference.getStddevDiff().orElse(null)); + } - public static List fromRunComparison(RunComparison comparison, - Map dimensionInfos) { + public static List fromRunComparison( + RunComparison comparison, Map dimensionInfos) { - return comparison.getDifferences().stream() - .map(diff -> fromDimensionDifference(diff, dimensionInfos)) - .collect(Collectors.toList()); - } + return comparison.getDifferences().stream() + .map(diff -> fromDimensionDifference(diff, dimensionInfos)) + .collect(Collectors.toList()); + } - public JsonDimension getDimension() { - return dimension; - } + public JsonDimension getDimension() { + return dimension; + } - public UUID getOldRunId() { - return oldRunId; - } + public UUID getOldRunId() { + return oldRunId; + } - public double getDiff() { - return diff; - } + public double getDiff() { + return diff; + } - @Nullable - public Double getReldiff() { - return reldiff; - } + @Nullable + public Double getReldiff() { + return reldiff; + } - @Nullable - public Double getStddevDiff() { - return stddevDiff; - } + @Nullable + public Double getStddevDiff() { + return stddevDiff; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionId.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionId.java index bd4a2a948..a6b277d55 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionId.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionId.java @@ -4,23 +4,23 @@ public class JsonDimensionId { - private final String benchmark; - private final String metric; + private final String benchmark; + private final String metric; - public JsonDimensionId(String benchmark, String metric) { - this.benchmark = benchmark; - this.metric = metric; - } + public JsonDimensionId(String benchmark, String metric) { + this.benchmark = benchmark; + this.metric = metric; + } - public String getBenchmark() { - return benchmark; - } + public String getBenchmark() { + return benchmark; + } - public String getMetric() { - return metric; - } + public String getMetric() { + return metric; + } - public Dimension toDimension() { - return new Dimension(getBenchmark(), getMetric()); - } + public Dimension toDimension() { + return new Dimension(getBenchmark(), getMetric()); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurement.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurement.java index 5626cc81c..23c909979 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurement.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurement.java @@ -8,100 +8,106 @@ public class JsonMeasurement { - private final JsonDimension dimension; - @Nullable - private final Double value; - @Nullable - private final List values; - @Nullable - private final String error; - @Nullable - private final Double stddev; - @Nullable - private final Double stddevPercent; - - private JsonMeasurement(JsonDimension dimension, @Nullable Double value, - @Nullable List values, @Nullable String error, @Nullable Double stddev, - @Nullable Double stddevPercent) { - - this.dimension = dimension; - this.value = value; - this.values = values; - this.error = error; - this.stddev = stddev; - this.stddevPercent = stddevPercent; - } - - public static JsonMeasurement successful(JsonDimension dimension, double value, - List values, Double stddev, Double stddevPercent) { - return new JsonMeasurement(dimension, value, values, null, stddev, stddevPercent); - } - - public static JsonMeasurement successful(JsonDimension dimension, double value) { - return new JsonMeasurement(dimension, value, null, null, null, null); - } - - public static JsonMeasurement failed(JsonDimension dimension, String error) { - return new JsonMeasurement(dimension, null, null, error, null, null); - } - - /** - * Create a {@link JsonMeasurement} from a {@link Measurement}. - * - * @param measurement the {@link Measurement} to use - * @param dimensionInfo the full info for the measurement's dimension - * @param significanceFactors the current significance factors (required for stddev - * calculations) - * @param allValues whether the full lists of values should also be included - * @return the newly created {@link JsonMeasurement} - */ - public static JsonMeasurement fromMeasurement(Measurement measurement, - DimensionInfo dimensionInfo, SignificanceFactors significanceFactors, boolean allValues) { - - if (!measurement.getDimension().equals(dimensionInfo.getDimension())) { - throw new IllegalArgumentException("measurement must have same dimension as dimension info"); - } - - JsonDimension dimension = JsonDimension.fromDimensionInfo(dimensionInfo); - - return measurement.getContent().consume( - left -> failed(dimension, left.getErrorMessage()), - right -> successful( - dimension, - right.getAverageValue(), - allValues ? right.getValues() : null, - right.getStddevWith(significanceFactors).orElse(null), - right.getStddevPercentWith(significanceFactors).orElse(null) - ) - ); - } - - public JsonDimension getDimension() { - return dimension; - } - - @Nullable - public Double getValue() { - return value; - } - - @Nullable - public List getValues() { - return values; - } - - @Nullable - public String getError() { - return error; - } - - @Nullable - public Double getStddev() { - return stddev; - } - - @Nullable - public Double getStddevPercent() { - return stddevPercent; - } + private final JsonDimension dimension; + @Nullable private final Double value; + @Nullable private final List values; + @Nullable private final String error; + @Nullable private final Double stddev; + @Nullable private final Double stddevPercent; + + private JsonMeasurement( + JsonDimension dimension, + @Nullable Double value, + @Nullable List values, + @Nullable String error, + @Nullable Double stddev, + @Nullable Double stddevPercent) { + + this.dimension = dimension; + this.value = value; + this.values = values; + this.error = error; + this.stddev = stddev; + this.stddevPercent = stddevPercent; + } + + public static JsonMeasurement successful( + JsonDimension dimension, + double value, + List values, + Double stddev, + Double stddevPercent) { + return new JsonMeasurement(dimension, value, values, null, stddev, stddevPercent); + } + + public static JsonMeasurement successful(JsonDimension dimension, double value) { + return new JsonMeasurement(dimension, value, null, null, null, null); + } + + public static JsonMeasurement failed(JsonDimension dimension, String error) { + return new JsonMeasurement(dimension, null, null, error, null, null); + } + + /** + * Create a {@link JsonMeasurement} from a {@link Measurement}. + * + * @param measurement the {@link Measurement} to use + * @param dimensionInfo the full info for the measurement's dimension + * @param significanceFactors the current significance factors (required for stddev calculations) + * @param allValues whether the full lists of values should also be included + * @return the newly created {@link JsonMeasurement} + */ + public static JsonMeasurement fromMeasurement( + Measurement measurement, + DimensionInfo dimensionInfo, + SignificanceFactors significanceFactors, + boolean allValues) { + + if (!measurement.getDimension().equals(dimensionInfo.getDimension())) { + throw new IllegalArgumentException("measurement must have same dimension as dimension info"); + } + + JsonDimension dimension = JsonDimension.fromDimensionInfo(dimensionInfo); + + return measurement + .getContent() + .consume( + left -> failed(dimension, left.getErrorMessage()), + right -> + successful( + dimension, + right.getAverageValue(), + allValues ? right.getValues() : null, + right.getStddevWith(significanceFactors).orElse(null), + right.getStddevPercentWith(significanceFactors).orElse(null))); + } + + public JsonDimension getDimension() { + return dimension; + } + + @Nullable + public Double getValue() { + return value; + } + + @Nullable + public List getValues() { + return values; + } + + @Nullable + public String getError() { + return error; + } + + @Nullable + public Double getStddev() { + return stddev; + } + + @Nullable + public Double getStddevPercent() { + return stddevPercent; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepo.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepo.java index a3c9cbfc6..6b01d33ed 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepo.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepo.java @@ -6,47 +6,51 @@ public class JsonRepo { - private final UUID id; - private final String name; - private final String remoteUrl; - private final List branches; - private final List dimensions; - @Nullable - private final Long lastGithubUpdate; - - public JsonRepo(UUID id, String name, String remoteUrl, List branches, - List dimensions, @Nullable Long lastGithubUpdate) { - - this.id = id; - this.name = name; - this.remoteUrl = remoteUrl; - this.branches = branches; - this.dimensions = dimensions; - this.lastGithubUpdate = lastGithubUpdate; - } - - public UUID getId() { - return id; - } - - public String getName() { - return name; - } - - public String getRemoteUrl() { - return remoteUrl; - } - - public List getBranches() { - return branches; - } - - public List getDimensions() { - return dimensions; - } - - @Nullable - public Long getLastGithubUpdate() { - return lastGithubUpdate; - } + private final UUID id; + private final String name; + private final String remoteUrl; + private final List branches; + private final List dimensions; + @Nullable private final Long lastGithubUpdate; + + public JsonRepo( + UUID id, + String name, + String remoteUrl, + List branches, + List dimensions, + @Nullable Long lastGithubUpdate) { + + this.id = id; + this.name = name; + this.remoteUrl = remoteUrl; + this.branches = branches; + this.dimensions = dimensions; + this.lastGithubUpdate = lastGithubUpdate; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public String getRemoteUrl() { + return remoteUrl; + } + + public List getBranches() { + return branches; + } + + public List getDimensions() { + return dimensions; + } + + @Nullable + public Long getLastGithubUpdate() { + return lastGithubUpdate; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResult.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResult.java index 9383df1cb..40ce37781 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResult.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResult.java @@ -13,90 +13,91 @@ public class JsonResult { - @Nullable - private final List measurements; - @Nullable - private final String benchError; - @Nullable - private final String velcomError; + @Nullable private final List measurements; + @Nullable private final String benchError; + @Nullable private final String velcomError; - private JsonResult(@Nullable List measurements, @Nullable String benchError, - @Nullable String velcomError) { + private JsonResult( + @Nullable List measurements, + @Nullable String benchError, + @Nullable String velcomError) { - this.measurements = measurements; - this.benchError = benchError; - this.velcomError = velcomError; - } + this.measurements = measurements; + this.benchError = benchError; + this.velcomError = velcomError; + } - public static JsonResult successful(List measurements) { - return new JsonResult(measurements, null, null); - } + public static JsonResult successful(List measurements) { + return new JsonResult(measurements, null, null); + } - public static JsonResult benchError(String benchError) { - return new JsonResult(null, benchError, null); - } + public static JsonResult benchError(String benchError) { + return new JsonResult(null, benchError, null); + } - public static JsonResult velcomError(String velcomError) { - return new JsonResult(null, null, velcomError); - } + public static JsonResult velcomError(String velcomError) { + return new JsonResult(null, null, velcomError); + } - /** - * Create a {@link JsonResult} from a {@link RunError}. - * - * @param error the error - * @return the newly created {@link JsonResult} - */ - public static JsonResult fromRunError(RunError error) { - switch (error.getType()) { - case BENCH_SCRIPT_ERROR: - return benchError(error.getMessage()); - case VELCOM_ERROR: - return velcomError(error.getMessage()); - default: - // This should never happen, but java sadly doesn't understand enums well enough to know - // that. Oh well - return velcomError("invalid result type"); - } - } + /** + * Create a {@link JsonResult} from a {@link RunError}. + * + * @param error the error + * @return the newly created {@link JsonResult} + */ + public static JsonResult fromRunError(RunError error) { + switch (error.getType()) { + case BENCH_SCRIPT_ERROR: + return benchError(error.getMessage()); + case VELCOM_ERROR: + return velcomError(error.getMessage()); + default: + // This should never happen, but java sadly doesn't understand enums well enough to know + // that. Oh well + return velcomError("invalid result type"); + } + } - /** - * Create a {@link JsonResult} from a list of measurements. - * - * @param measurements the measurements to use - * @param dimensionInfos the dimensions for each measurement - * @param significanceFactors the current significance factors (required for stddev - * calculations) - * @param allValues whether the full lists of values should also be included for each - * measurement - * @return the newly created {@link JsonResult} - */ - public static JsonResult fromMeasurements(Collection measurements, - Map dimensionInfos, SignificanceFactors significanceFactors, - boolean allValues) { + /** + * Create a {@link JsonResult} from a list of measurements. + * + * @param measurements the measurements to use + * @param dimensionInfos the dimensions for each measurement + * @param significanceFactors the current significance factors (required for stddev calculations) + * @param allValues whether the full lists of values should also be included for each measurement + * @return the newly created {@link JsonResult} + */ + public static JsonResult fromMeasurements( + Collection measurements, + Map dimensionInfos, + SignificanceFactors significanceFactors, + boolean allValues) { - List jsonMeasurements = measurements.stream() - .map(measurement -> JsonMeasurement.fromMeasurement( - measurement, - dimensionInfos.get(measurement.getDimension()), - significanceFactors, - allValues - )) - .collect(Collectors.toList()); - return successful(jsonMeasurements); - } + List jsonMeasurements = + measurements.stream() + .map( + measurement -> + JsonMeasurement.fromMeasurement( + measurement, + dimensionInfos.get(measurement.getDimension()), + significanceFactors, + allValues)) + .collect(Collectors.toList()); + return successful(jsonMeasurements); + } - @Nullable - public List getMeasurements() { - return measurements; - } + @Nullable + public List getMeasurements() { + return measurements; + } - @Nullable - public String getBenchError() { - return benchError; - } + @Nullable + public String getBenchError() { + return benchError; + } - @Nullable - public String getVelcomError() { - return velcomError; - } + @Nullable + public String getVelcomError() { + return velcomError; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRun.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRun.java index 11c3e1dc5..44a43c4f4 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRun.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRun.java @@ -4,57 +4,64 @@ public class JsonRun { - private final UUID id; - private final String author; - private final String runnerName; - private final String runnerInfo; - private final long startTime; - private final long stopTime; - private final JsonSource source; - private final JsonResult result; - - public JsonRun(UUID id, String author, String runnerName, String runnerInfo, long startTime, - long stopTime, JsonSource source, JsonResult result) { - - this.id = id; - this.author = author; - this.runnerName = runnerName; - this.runnerInfo = runnerInfo; - this.startTime = startTime; - this.stopTime = stopTime; - this.source = source; - this.result = result; - } - - public UUID getId() { - return id; - } - - public String getAuthor() { - return author; - } - - public String getRunnerName() { - return runnerName; - } - - public String getRunnerInfo() { - return runnerInfo; - } - - public long getStartTime() { - return startTime; - } - - public long getStopTime() { - return stopTime; - } - - public JsonSource getSource() { - return source; - } - - public JsonResult getResult() { - return result; - } + private final UUID id; + private final String author; + private final String runnerName; + private final String runnerInfo; + private final long startTime; + private final long stopTime; + private final JsonSource source; + private final JsonResult result; + + public JsonRun( + UUID id, + String author, + String runnerName, + String runnerInfo, + long startTime, + long stopTime, + JsonSource source, + JsonResult result) { + + this.id = id; + this.author = author; + this.runnerName = runnerName; + this.runnerInfo = runnerInfo; + this.startTime = startTime; + this.stopTime = stopTime; + this.source = source; + this.result = result; + } + + public UUID getId() { + return id; + } + + public String getAuthor() { + return author; + } + + public String getRunnerName() { + return runnerName; + } + + public String getRunnerInfo() { + return runnerInfo; + } + + public long getStartTime() { + return startTime; + } + + public long getStopTime() { + return stopTime; + } + + public JsonSource getSource() { + return source; + } + + public JsonResult getResult() { + return result; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescription.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescription.java index 4795eaa39..cc0f931df 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescription.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescription.java @@ -8,41 +8,45 @@ public class JsonRunDescription { - private final UUID id; - private final long startTime; - private final JsonSuccess success; - private final JsonSource source; - - public JsonRunDescription(UUID id, long startTime, JsonSuccess success, JsonSource source) { - this.id = id; - this.startTime = startTime; - this.success = success; - this.source = source; - } - - public UUID getId() { - return id; - } - - public long getStartTime() { - return startTime; - } - - public JsonSuccess getSuccess() { - return success; - } - - public JsonSource getSource() { - return source; - } - - public enum JsonSuccess { - SUCCESS, PARTIAL_SUCCESS, FAILURE; - - public static JsonSuccess fromRunResult(Either> result) { - return result.getRight() - .map(ms -> ms.stream().allMatch(m -> m.getContent().isRight()) ? SUCCESS : PARTIAL_SUCCESS) - .orElse(FAILURE); - } - } + private final UUID id; + private final long startTime; + private final JsonSuccess success; + private final JsonSource source; + + public JsonRunDescription(UUID id, long startTime, JsonSuccess success, JsonSource source) { + this.id = id; + this.startTime = startTime; + this.success = success; + this.source = source; + } + + public UUID getId() { + return id; + } + + public long getStartTime() { + return startTime; + } + + public JsonSuccess getSuccess() { + return success; + } + + public JsonSource getSource() { + return source; + } + + public enum JsonSuccess { + SUCCESS, + PARTIAL_SUCCESS, + FAILURE; + + public static JsonSuccess fromRunResult(Either> result) { + return result + .getRight() + .map( + ms -> ms.stream().allMatch(m -> m.getContent().isRight()) ? SUCCESS : PARTIAL_SUCCESS) + .orElse(FAILURE); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunner.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunner.java index 4fb8263dc..b9237c509 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunner.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunner.java @@ -8,53 +8,53 @@ public class JsonRunner { - private final String name; - private final String info; - @Nullable - private final UUID workingOn; - @Nullable - private final Long workingSince; - private final boolean lostConnection; - - public JsonRunner(String name, String info, @Nullable UUID workingOn, - @Nullable Long workingSince, boolean lostConnection) { - this.name = Objects.requireNonNull(name, "name can not be null!"); - this.info = Objects.requireNonNull(info, "info can not be null!"); - this.workingOn = workingOn; - this.workingSince = workingSince; - this.lostConnection = lostConnection; - } - - public static JsonRunner fromKnownRunner(KnownRunner runner) { - return new JsonRunner( - runner.getName(), - runner.getInformation(), - runner.getCurrentTask().map(it -> it.getId().getId()).orElse(null), - runner.getWorkingSince().map(Instant::getEpochSecond).orElse(null), - runner.hasLostConnection() - ); - } - - - public String getName() { - return name; - } - - public String getInfo() { - return info; - } - - @Nullable - public UUID getWorkingOn() { - return workingOn; - } - - @Nullable - public Long getWorkingSince() { - return workingSince; - } - - public boolean isLostConnection() { - return lostConnection; - } + private final String name; + private final String info; + @Nullable private final UUID workingOn; + @Nullable private final Long workingSince; + private final boolean lostConnection; + + public JsonRunner( + String name, + String info, + @Nullable UUID workingOn, + @Nullable Long workingSince, + boolean lostConnection) { + this.name = Objects.requireNonNull(name, "name can not be null!"); + this.info = Objects.requireNonNull(info, "info can not be null!"); + this.workingOn = workingOn; + this.workingSince = workingSince; + this.lostConnection = lostConnection; + } + + public static JsonRunner fromKnownRunner(KnownRunner runner) { + return new JsonRunner( + runner.getName(), + runner.getInformation(), + runner.getCurrentTask().map(it -> it.getId().getId()).orElse(null), + runner.getWorkingSince().map(Instant::getEpochSecond).orElse(null), + runner.hasLostConnection()); + } + + public String getName() { + return name; + } + + public String getInfo() { + return info; + } + + @Nullable + public UUID getWorkingOn() { + return workingOn; + } + + @Nullable + public Long getWorkingSince() { + return workingSince; + } + + public boolean isLostConnection() { + return lostConnection; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonShortRunDescription.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonShortRunDescription.java index e667f9097..356cde6ed 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonShortRunDescription.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonShortRunDescription.java @@ -6,49 +6,47 @@ public class JsonShortRunDescription { - private final UUID id; - @Nullable - private final String commitHash; - @Nullable - private final String commitSummary; - @Nullable - private final String tarDescription; - - public JsonShortRunDescription(UUID id, @Nullable String commitHash, - @Nullable String commitSummary, - @Nullable String tarDescription) { - - this.id = id; - this.commitHash = commitHash; - this.commitSummary = commitSummary; - this.tarDescription = tarDescription; - } - - public static JsonShortRunDescription fromShortRunDescription(ShortRunDescription description) { - return new JsonShortRunDescription( - description.getId().getId(), - description.getCommitHash().orElse(null), - description.getCommitSummary().orElse(null), - description.getTarDescription().orElse(null) - ); - } - - public UUID getId() { - return id; - } - - @Nullable - public String getCommitHash() { - return commitHash; - } - - @Nullable - public String getCommitSummary() { - return commitSummary; - } - - @Nullable - public String getTarDescription() { - return tarDescription; - } + private final UUID id; + @Nullable private final String commitHash; + @Nullable private final String commitSummary; + @Nullable private final String tarDescription; + + public JsonShortRunDescription( + UUID id, + @Nullable String commitHash, + @Nullable String commitSummary, + @Nullable String tarDescription) { + + this.id = id; + this.commitHash = commitHash; + this.commitSummary = commitSummary; + this.tarDescription = tarDescription; + } + + public static JsonShortRunDescription fromShortRunDescription(ShortRunDescription description) { + return new JsonShortRunDescription( + description.getId().getId(), + description.getCommitHash().orElse(null), + description.getCommitSummary().orElse(null), + description.getTarDescription().orElse(null)); + } + + public UUID getId() { + return id; + } + + @Nullable + public String getCommitHash() { + return commitHash; + } + + @Nullable + public String getCommitSummary() { + return commitSummary; + } + + @Nullable + public String getTarDescription() { + return tarDescription; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSource.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSource.java index a64e28f26..30ae8df1a 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSource.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSource.java @@ -11,85 +11,82 @@ public class JsonSource { - private final JsonSourceType type; - - @Nullable - private final JsonCommitDescription commitDescription; - @Nullable - private final JsonSourceUploadedTar uploadedTar; - - private JsonSource(JsonSourceType type, @Nullable JsonCommitDescription commitDescription, - @Nullable JsonSourceUploadedTar uploadedTar) { - - this.type = type; - this.commitDescription = commitDescription; - this.uploadedTar = uploadedTar; - } - - public static JsonSource commitSource(JsonCommitDescription commitDescription) { - return new JsonSource(JsonSourceType.COMMIT, commitDescription, null); - } - - public static JsonSource tarSource(String description, @Nullable UUID repoId) { - return new JsonSource(JsonSourceType.UPLOADED_TAR, null, - new JsonSourceUploadedTar(description, repoId)); - } - - public static JsonSource fromCommit(Commit commit) { - return commitSource(JsonCommitDescription.fromCommit(commit)); - } - - public static JsonSource fromTarSource(TarSource source) { - return tarSource( - source.getDescription(), - source.getRepoId().map(RepoId::getId).orElse(null) - ); - } - - public static JsonSource fromSource(Either source, - CommitReadAccess commitAccess) { - - return source - .mapLeft(it -> commitAccess.getCommit(it.getRepoId(), it.getHash())) - .consume(JsonSource::fromCommit, JsonSource::fromTarSource); - } - - public JsonSourceType getType() { - return type; - } - - public Object getSource() { - switch (type) { - case COMMIT: - return commitDescription; - case UPLOADED_TAR: - return uploadedTar; - } - return null; - } - - private enum JsonSourceType { - COMMIT, UPLOADED_TAR - } - - private static class JsonSourceUploadedTar { - - private final String description; - @Nullable - private final UUID repoId; - - public JsonSourceUploadedTar(String description, @Nullable UUID repoId) { - this.description = description; - this.repoId = repoId; - } - - public String getDescription() { - return description; - } - - @Nullable - public UUID getRepoId() { - return repoId; - } - } + private final JsonSourceType type; + + @Nullable private final JsonCommitDescription commitDescription; + @Nullable private final JsonSourceUploadedTar uploadedTar; + + private JsonSource( + JsonSourceType type, + @Nullable JsonCommitDescription commitDescription, + @Nullable JsonSourceUploadedTar uploadedTar) { + + this.type = type; + this.commitDescription = commitDescription; + this.uploadedTar = uploadedTar; + } + + public static JsonSource commitSource(JsonCommitDescription commitDescription) { + return new JsonSource(JsonSourceType.COMMIT, commitDescription, null); + } + + public static JsonSource tarSource(String description, @Nullable UUID repoId) { + return new JsonSource( + JsonSourceType.UPLOADED_TAR, null, new JsonSourceUploadedTar(description, repoId)); + } + + public static JsonSource fromCommit(Commit commit) { + return commitSource(JsonCommitDescription.fromCommit(commit)); + } + + public static JsonSource fromTarSource(TarSource source) { + return tarSource(source.getDescription(), source.getRepoId().map(RepoId::getId).orElse(null)); + } + + public static JsonSource fromSource( + Either source, CommitReadAccess commitAccess) { + + return source + .mapLeft(it -> commitAccess.getCommit(it.getRepoId(), it.getHash())) + .consume(JsonSource::fromCommit, JsonSource::fromTarSource); + } + + public JsonSourceType getType() { + return type; + } + + public Object getSource() { + switch (type) { + case COMMIT: + return commitDescription; + case UPLOADED_TAR: + return uploadedTar; + } + return null; + } + + private enum JsonSourceType { + COMMIT, + UPLOADED_TAR + } + + private static class JsonSourceUploadedTar { + + private final String description; + @Nullable private final UUID repoId; + + public JsonSourceUploadedTar(String description, @Nullable UUID repoId) { + this.description = description; + this.repoId = repoId; + } + + public String getDescription() { + return description; + } + + @Nullable + public UUID getRepoId() { + return repoId; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTask.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTask.java index 3397638f9..512c12097 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTask.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTask.java @@ -6,46 +6,44 @@ public class JsonTask { - private final UUID id; - private final String author; - private final long since; - private final JsonSource source; - - public JsonTask(UUID id, String author, long since, JsonSource source) { - this.id = id; - this.author = author; - this.since = since; - this.source = source; - } - - public static JsonTask fromTask(Task task, JsonSource source) { - return new JsonTask( - task.getId().getId(), - task.getAuthor(), - task.getInsertTime().getEpochSecond(), - source - ); - } - - public static JsonTask fromTask(Task task, CommitReadAccess commitAccess) { - return fromTask(task, task.getSource() - .mapLeft(it -> commitAccess.getCommit(it.getRepoId(), it.getHash())) - .consume(JsonSource::fromCommit, JsonSource::fromTarSource)); - } - - public UUID getId() { - return id; - } - - public String getAuthor() { - return author; - } - - public long getSince() { - return since; - } - - public JsonSource getSource() { - return source; - } + private final UUID id; + private final String author; + private final long since; + private final JsonSource source; + + public JsonTask(UUID id, String author, long since, JsonSource source) { + this.id = id; + this.author = author; + this.since = since; + this.source = source; + } + + public static JsonTask fromTask(Task task, JsonSource source) { + return new JsonTask( + task.getId().getId(), task.getAuthor(), task.getInsertTime().getEpochSecond(), source); + } + + public static JsonTask fromTask(Task task, CommitReadAccess commitAccess) { + return fromTask( + task, + task.getSource() + .mapLeft(it -> commitAccess.getCommit(it.getRepoId(), it.getHash())) + .consume(JsonSource::fromCommit, JsonSource::fromTarSource)); + } + + public UUID getId() { + return id; + } + + public String getAuthor() { + return author; + } + + public long getSince() { + return since; + } + + public JsonSource getSource() { + return source; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/logging/MetricsEndpointFilter.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/logging/MetricsEndpointFilter.java index 74864c984..9d3f52f6e 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/logging/MetricsEndpointFilter.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/restapi/logging/MetricsEndpointFilter.java @@ -11,17 +11,17 @@ @JsonTypeName("metrics-endpoint") public class MetricsEndpointFilter implements FilterFactory { - @Override - public Filter build() { - return new Filter<>() { - @Override - public FilterReply decide(IAccessEvent event) { - if (event.getRequestURI().equals("/prometheusMetrics")) { - return FilterReply.DENY; - } else { - return FilterReply.NEUTRAL; - } - } - }; - } + @Override + public Filter build() { + return new Filter<>() { + @Override + public FilterReply decide(IAccessEvent event) { + if (event.getRequestURI().equals("/prometheusMetrics")) { + return FilterReply.DENY; + } else { + return FilterReply.NEUTRAL; + } + } + }; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Delays.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Delays.java index e7dc5f824..6e36d5dc7 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Delays.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Delays.java @@ -2,29 +2,25 @@ import java.time.Duration; -/** - * A class for keeping all kinds of runner-related delays and timeouts. - */ +/** A class for keeping all kinds of runner-related delays and timeouts. */ public final class Delays { - /** - * How long to wait for a runner response when trying to close a connection before force-closing - * it. - */ - public static final Duration CLOSE_CONNECTION_TIMEOUT = Duration.ofSeconds(10); + /** + * How long to wait for a runner response when trying to close a connection before force-closing + * it. + */ + public static final Duration CLOSE_CONNECTION_TIMEOUT = Duration.ofSeconds(10); - /** - * How long to wait - after sending a command to the server - for a reply to that command. - */ - public static final Duration AWAIT_COMMAND_REPLY = Duration.ofSeconds(10); + /** How long to wait - after sending a command to the server - for a reply to that command. */ + public static final Duration AWAIT_COMMAND_REPLY = Duration.ofSeconds(10); - /** - * How long to wait between "get_status" commands. This affects the update interval of the live - * build log. - */ - public static final Duration REQUEST_STATUS_INTERVAL = Duration.ofSeconds(5); + /** + * How long to wait between "get_status" commands. This affects the update interval of the live + * build log. + */ + public static final Duration REQUEST_STATUS_INTERVAL = Duration.ofSeconds(5); - private Delays() { - throw new UnsupportedOperationException(); - } + private Delays() { + throw new UnsupportedOperationException(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Dispatcher.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Dispatcher.java index 2dc95b41f..f2d2b6627 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Dispatcher.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/Dispatcher.java @@ -22,158 +22,157 @@ public class Dispatcher implements IDispatcher { - private static final Logger LOGGER = LoggerFactory.getLogger(Dispatcher.class); - - private final List teleRunners; - private final Queue queue; - private final Duration disconnectedRunnerGracePeriod; - - public Dispatcher(Queue queue, Duration disconnectedRunnerGracePeriod) { - this.queue = queue; - this.disconnectedRunnerGracePeriod = disconnectedRunnerGracePeriod; - this.teleRunners = new ArrayList<>(); - - ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor( - new DaemonThreadFactory() - ); - executor.scheduleAtFixedRate( - this::cleanupDisconnectedRunners, - 0, - Math.max(disconnectedRunnerGracePeriod.toSeconds() / 2, 1), - TimeUnit.SECONDS - ); - } - - private void cleanupDisconnectedRunners() { - synchronized (teleRunners) { - LOGGER.debug("Checking for disconnected runners (I know {} runners)", teleRunners.size()); - Predicate outOfGracePeriod = runner -> { - Duration timeSinceLastPing = Duration.between(runner.getLastPing(), Instant.now()); - return timeSinceLastPing.compareTo(disconnectedRunnerGracePeriod) > 0; - }; - - List runnersToRemove = new ArrayList<>(); - - for (TeleRunner runner : teleRunners) { - // We synchronize on the runner object. If a runner with this name joins the same object - // would be returned if the cleanup synchronized block was not yet entered. - // In this case we would remove the runner from the dispatchers teleRunners list but it - // would get a new connection and execute work ==> Bad - // So we mark the runner as disposed and synchronize on it. If the new runner connects - // while we are here, either we go first and dispose the runner or the listener comes first - // and sets a new connection. This leads us to bail out of the if and not delete the runner. - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (runner) { - if (!runner.hasConnection() && outOfGracePeriod.test(runner)) { - LOGGER.info("Removing disconnected runner {}", runner.getRunnerName()); - runner.dispose(); - runnersToRemove.add(runner); - } - } - } - - teleRunners.removeAll(runnersToRemove); - } - } - - /** - * Adds a runner to the dispatcher. - * - * @param teleRunner the runner to add - * @throws IllegalArgumentException if the runner name is already taken - */ - public void addRunner(TeleRunner teleRunner) { - synchronized (teleRunners) { - boolean nameTaken = teleRunners.stream() - .filter(TeleRunner::hasConnection) - .map(TeleRunner::getRunnerName) - .anyMatch(it -> it.equals(teleRunner.getRunnerName())); - - if (nameTaken) { - throw new IllegalArgumentException("The runner name is already taken!"); - } - - teleRunners.add(teleRunner); - } - } - - @Override - public List getKnownRunners() { - synchronized (teleRunners) { - return teleRunners.stream() - .map(TeleRunner::getRunnerInformation) - .collect(Collectors.toList()); - } - } - - /** - * Marks a task as completed. - * - * @param result the resulting run - */ - public void completeTask(NewRun result) { - queue.completeTask(result); - } - - /** - * Returns the next work the given {@link TeleRunner} should execute and registers it as working. - * - * @param runner the runner to execute the work on - * @return the next available task. - */ - public Optional getWork(TeleRunner runner) { - synchronized (teleRunners) { - // We do not know the runner yet or we know an old version of it apparently. - Optional knownRunner = getTeleRunner(runner.getRunnerName()); - if (knownRunner.isEmpty() || knownRunner.get() != runner) { - return Optional.empty(); - } - } - Optional nextTask = queue.startNextTask(); - if (nextTask.isEmpty()) { - return Optional.empty(); - } - - return nextTask; - } - - public Optional findLinesForTask(UUID taskId) { - Optional activeWorker = getKnownRunners().stream() - .filter(it -> it.getCurrentTask().isPresent()) - .filter(it -> it.getCurrentTask().get().getId().getId().equals(taskId)) - .findAny(); - - if (activeWorker.isPresent()) { - LinesWithOffset lastOutputLines = activeWorker.get().getLastOutputLines() - .orElse(new LinesWithOffset(0, List.of())); - return Optional.of(lastOutputLines); - } - - return getKnownRunners().stream() - .flatMap(it -> it.getCompletedTasks().stream()) - .filter(it -> it.getTaskId().getId().equals(taskId)) - .findFirst() - .map(it -> it.getLastLogLines().orElse(new LinesWithOffset(0, List.of()))); - } - - /** - * Returns the {@link TeleRunner} for a given name. - * - * @param name the name of the runner - * @return the runner or an empty optional otherwise - */ - Optional getTeleRunner(String name) { - synchronized (teleRunners) { - return teleRunners.stream() - .filter(it -> it.getRunnerName().equals(name)) - .findAny(); - } - } - - /** - * @return the queue the dispatcher uses - */ - public Queue getQueue() { - return queue; - } + private static final Logger LOGGER = LoggerFactory.getLogger(Dispatcher.class); + + private final List teleRunners; + private final Queue queue; + private final Duration disconnectedRunnerGracePeriod; + + public Dispatcher(Queue queue, Duration disconnectedRunnerGracePeriod) { + this.queue = queue; + this.disconnectedRunnerGracePeriod = disconnectedRunnerGracePeriod; + this.teleRunners = new ArrayList<>(); + + ScheduledExecutorService executor = + Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory()); + executor.scheduleAtFixedRate( + this::cleanupDisconnectedRunners, + 0, + Math.max(disconnectedRunnerGracePeriod.toSeconds() / 2, 1), + TimeUnit.SECONDS); + } + + private void cleanupDisconnectedRunners() { + synchronized (teleRunners) { + LOGGER.debug("Checking for disconnected runners (I know {} runners)", teleRunners.size()); + Predicate outOfGracePeriod = + runner -> { + Duration timeSinceLastPing = Duration.between(runner.getLastPing(), Instant.now()); + return timeSinceLastPing.compareTo(disconnectedRunnerGracePeriod) > 0; + }; + + List runnersToRemove = new ArrayList<>(); + + for (TeleRunner runner : teleRunners) { + // We synchronize on the runner object. If a runner with this name joins the same object + // would be returned if the cleanup synchronized block was not yet entered. + // In this case we would remove the runner from the dispatchers teleRunners list but it + // would get a new connection and execute work ==> Bad + // So we mark the runner as disposed and synchronize on it. If the new runner connects + // while we are here, either we go first and dispose the runner or the listener comes first + // and sets a new connection. This leads us to bail out of the if and not delete the runner. + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (runner) { + if (!runner.hasConnection() && outOfGracePeriod.test(runner)) { + LOGGER.info("Removing disconnected runner {}", runner.getRunnerName()); + runner.dispose(); + runnersToRemove.add(runner); + } + } + } + + teleRunners.removeAll(runnersToRemove); + } + } + + /** + * Adds a runner to the dispatcher. + * + * @param teleRunner the runner to add + * @throws IllegalArgumentException if the runner name is already taken + */ + public void addRunner(TeleRunner teleRunner) { + synchronized (teleRunners) { + boolean nameTaken = + teleRunners.stream() + .filter(TeleRunner::hasConnection) + .map(TeleRunner::getRunnerName) + .anyMatch(it -> it.equals(teleRunner.getRunnerName())); + + if (nameTaken) { + throw new IllegalArgumentException("The runner name is already taken!"); + } + + teleRunners.add(teleRunner); + } + } + + @Override + public List getKnownRunners() { + synchronized (teleRunners) { + return teleRunners.stream() + .map(TeleRunner::getRunnerInformation) + .collect(Collectors.toList()); + } + } + + /** + * Marks a task as completed. + * + * @param result the resulting run + */ + public void completeTask(NewRun result) { + queue.completeTask(result); + } + + /** + * Returns the next work the given {@link TeleRunner} should execute and registers it as working. + * + * @param runner the runner to execute the work on + * @return the next available task. + */ + public Optional getWork(TeleRunner runner) { + synchronized (teleRunners) { + // We do not know the runner yet or we know an old version of it apparently. + Optional knownRunner = getTeleRunner(runner.getRunnerName()); + if (knownRunner.isEmpty() || knownRunner.get() != runner) { + return Optional.empty(); + } + } + Optional nextTask = queue.startNextTask(); + if (nextTask.isEmpty()) { + return Optional.empty(); + } + + return nextTask; + } + + public Optional findLinesForTask(UUID taskId) { + Optional activeWorker = + getKnownRunners().stream() + .filter(it -> it.getCurrentTask().isPresent()) + .filter(it -> it.getCurrentTask().get().getId().getId().equals(taskId)) + .findAny(); + + if (activeWorker.isPresent()) { + LinesWithOffset lastOutputLines = + activeWorker.get().getLastOutputLines().orElse(new LinesWithOffset(0, List.of())); + return Optional.of(lastOutputLines); + } + + return getKnownRunners().stream() + .flatMap(it -> it.getCompletedTasks().stream()) + .filter(it -> it.getTaskId().getId().equals(taskId)) + .findFirst() + .map(it -> it.getLastLogLines().orElse(new LinesWithOffset(0, List.of()))); + } + + /** + * Returns the {@link TeleRunner} for a given name. + * + * @param name the name of the runner + * @return the runner or an empty optional otherwise + */ + Optional getTeleRunner(String name) { + synchronized (teleRunners) { + return teleRunners.stream().filter(it -> it.getRunnerName().equals(name)).findAny(); + } + } + + /** + * @return the queue the dispatcher uses + */ + public Queue getQueue() { + return queue; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/IDispatcher.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/IDispatcher.java index 116aa70c2..4f0e7e235 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/IDispatcher.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/IDispatcher.java @@ -5,20 +5,18 @@ import java.util.Optional; import java.util.UUID; -/** - * The dispatcher interface other modules can compile against. - */ +/** The dispatcher interface other modules can compile against. */ public interface IDispatcher { - /** - * @return a list with all known runners - */ - List getKnownRunners(); + /** + * @return a list with all known runners + */ + List getKnownRunners(); - /** - * @param taskId the task to find the last log lines for - * @return the last output lines for a given task. Considers live runners and the last few - * finished results of each runner. - */ - Optional findLinesForTask(UUID taskId); + /** + * @param taskId the task to find the last log lines for + * @return the last output lines for a given task. Considers live runners and the last few + * finished results of each runner. + */ + Optional findLinesForTask(UUID taskId); } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/KnownRunner.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/KnownRunner.java index a83efac31..65fdbfe12 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/KnownRunner.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/KnownRunner.java @@ -10,159 +10,167 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * A runner that is known to the dispatcher. - */ +/** A runner that is known to the dispatcher. */ public class KnownRunner { - private final String name; - private final String information; - @Nullable - private final String versionHash; - private final Status lastStatus; - private final boolean lostConnection; - @Nullable - private final Task currentTask; - @Nullable - final LinesWithOffset lastOutputLines; - @Nullable - private final Instant workingSince; - private final List completedTasks; - - /** - * Creates a new known runner. - * - * @param name the name of the runner - * @param information the runner information - * @param versionHash the commit hash the runner was built on - * @param lastStatus the last known runner status - * @param task the task the runner is currently working on - * @param lostConnection true if the connection to the runner is lost - * @param workingSince the time the runner is working on a run now - * @param lastOutputLines the last output lines - * @param completedTasks the last few completed tasks - */ - public KnownRunner(String name, String information, @Nullable String versionHash, - Status lastStatus, @Nullable Task task, boolean lostConnection, - @Nullable Instant workingSince, @Nullable LinesWithOffset lastOutputLines, - List completedTasks) { - this.name = Objects.requireNonNull(name, "name can not be null!"); - this.information = Objects.requireNonNull(information, "information can not be null!"); - this.versionHash = versionHash; - this.lastStatus = Objects.requireNonNull(lastStatus, "status can not be null!"); - this.currentTask = task; - this.lostConnection = lostConnection; - this.workingSince = workingSince; - this.lastOutputLines = lastOutputLines; - this.completedTasks = completedTasks; - } - - public String getName() { - return name; - } - - public String getInformation() { - return information; - } - - public Optional getVersionHash() { - return Optional.ofNullable(versionHash); - } - - /** - * @return the last known status. Might be out of date if {@link #hasLostConnection()} is true - */ - public Status getLastStatus() { - return lastStatus; - } - - public Optional getCurrentTask() { - return Optional.ofNullable(currentTask); - } - - /** - * @return true if runner is currently disconnected - */ - public boolean hasLostConnection() { - return lostConnection; - } - - /** - * @return the last output lines - */ - public Optional getLastOutputLines() { - return Optional.ofNullable(lastOutputLines); - } - - /** - * @return the duration since the runner started working on a run. Includes transfer time. - */ - public Optional getWorkingSince() { - return Optional.ofNullable(workingSince); - } - - /** - * @return the last few completed tasks - */ - public List getCompletedTasks() { - return completedTasks; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - KnownRunner that = (KnownRunner) o; - return lostConnection == that.lostConnection && - Objects.equals(name, that.name) && - Objects.equals(information, that.information) && - Objects.equals(versionHash, that.versionHash) && - lastStatus == that.lastStatus && - Objects.equals(currentTask, that.currentTask) && - Objects.equals(workingSince, that.workingSince); - } - - @Override - public int hashCode() { - return Objects.hash( - name, information, versionHash, lastStatus, lostConnection, currentTask, workingSince - ); - } - - @Override - public String toString() { - return "KnownRunner{" + - "name='" + name + '\'' + - ", information='" + information + '\'' + - ", versionHash='" + versionHash + '\'' + - ", lastStatus=" + lastStatus + - ", lostConnection=" + lostConnection + - ", currentTask=" + currentTask + - ", workingSince=" + workingSince + - '}'; - } - - public static class CompletedTask { - - private final TaskId taskId; - @Nullable - private final LinesWithOffset lastLogLines; - - public CompletedTask(TaskId taskId, @Nullable LinesWithOffset lastLogLines) { - this.taskId = taskId; - this.lastLogLines = lastLogLines; - } - - public TaskId getTaskId() { - return taskId; - } - - public Optional getLastLogLines() { - return Optional.ofNullable(lastLogLines); - } - } + private final String name; + private final String information; + @Nullable private final String versionHash; + private final Status lastStatus; + private final boolean lostConnection; + @Nullable private final Task currentTask; + @Nullable final LinesWithOffset lastOutputLines; + @Nullable private final Instant workingSince; + private final List completedTasks; + + /** + * Creates a new known runner. + * + * @param name the name of the runner + * @param information the runner information + * @param versionHash the commit hash the runner was built on + * @param lastStatus the last known runner status + * @param task the task the runner is currently working on + * @param lostConnection true if the connection to the runner is lost + * @param workingSince the time the runner is working on a run now + * @param lastOutputLines the last output lines + * @param completedTasks the last few completed tasks + */ + public KnownRunner( + String name, + String information, + @Nullable String versionHash, + Status lastStatus, + @Nullable Task task, + boolean lostConnection, + @Nullable Instant workingSince, + @Nullable LinesWithOffset lastOutputLines, + List completedTasks) { + this.name = Objects.requireNonNull(name, "name can not be null!"); + this.information = Objects.requireNonNull(information, "information can not be null!"); + this.versionHash = versionHash; + this.lastStatus = Objects.requireNonNull(lastStatus, "status can not be null!"); + this.currentTask = task; + this.lostConnection = lostConnection; + this.workingSince = workingSince; + this.lastOutputLines = lastOutputLines; + this.completedTasks = completedTasks; + } + + public String getName() { + return name; + } + + public String getInformation() { + return information; + } + + public Optional getVersionHash() { + return Optional.ofNullable(versionHash); + } + + /** + * @return the last known status. Might be out of date if {@link #hasLostConnection()} is true + */ + public Status getLastStatus() { + return lastStatus; + } + + public Optional getCurrentTask() { + return Optional.ofNullable(currentTask); + } + + /** + * @return true if runner is currently disconnected + */ + public boolean hasLostConnection() { + return lostConnection; + } + + /** + * @return the last output lines + */ + public Optional getLastOutputLines() { + return Optional.ofNullable(lastOutputLines); + } + + /** + * @return the duration since the runner started working on a run. Includes transfer time. + */ + public Optional getWorkingSince() { + return Optional.ofNullable(workingSince); + } + + /** + * @return the last few completed tasks + */ + public List getCompletedTasks() { + return completedTasks; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KnownRunner that = (KnownRunner) o; + return lostConnection == that.lostConnection + && Objects.equals(name, that.name) + && Objects.equals(information, that.information) + && Objects.equals(versionHash, that.versionHash) + && lastStatus == that.lastStatus + && Objects.equals(currentTask, that.currentTask) + && Objects.equals(workingSince, that.workingSince); + } + + @Override + public int hashCode() { + return Objects.hash( + name, information, versionHash, lastStatus, lostConnection, currentTask, workingSince); + } + + @Override + public String toString() { + return "KnownRunner{" + + "name='" + + name + + '\'' + + ", information='" + + information + + '\'' + + ", versionHash='" + + versionHash + + '\'' + + ", lastStatus=" + + lastStatus + + ", lostConnection=" + + lostConnection + + ", currentTask=" + + currentTask + + ", workingSince=" + + workingSince + + '}'; + } + + public static class CompletedTask { + + private final TaskId taskId; + @Nullable private final LinesWithOffset lastLogLines; + + public CompletedTask(TaskId taskId, @Nullable LinesWithOffset lastLogLines) { + this.taskId = taskId; + this.lastLogLines = lastLogLines; + } + + public TaskId getTaskId() { + return taskId; + } + + public Optional getLastLogLines() { + return Optional.ofNullable(lastLogLines); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServlet.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServlet.java index 77c537fc0..4cd5b3d8f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServlet.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServlet.java @@ -13,98 +13,96 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A servlet that sets up the server side runner communication servlet. - */ +/** A servlet that sets up the server side runner communication servlet. */ public class ServerMasterWebsocketServlet extends WebSocketServlet { - private static final Logger LOGGER = LoggerFactory.getLogger(ServerMasterWebsocketServlet.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ServerMasterWebsocketServlet.class); - private final Dispatcher dispatcher; - private final Serializer serializer; - private final String runnerToken; - private final BenchRepo benchRepo; + private final Dispatcher dispatcher; + private final Serializer serializer; + private final String runnerToken; + private final BenchRepo benchRepo; - /** - * Creates a new runner servlet. - * - * @param dispatcher the dispatcher to connect to - * @param serializer the serializer to use for communication - * @param runnerToken the token runners need to provide when connecting - * @param benchRepo the benchmark repo - */ - public ServerMasterWebsocketServlet(Dispatcher dispatcher, Serializer serializer, - String runnerToken, BenchRepo benchRepo) { - this.dispatcher = dispatcher; - this.serializer = serializer; - this.runnerToken = runnerToken; - this.benchRepo = benchRepo; - } + /** + * Creates a new runner servlet. + * + * @param dispatcher the dispatcher to connect to + * @param serializer the serializer to use for communication + * @param runnerToken the token runners need to provide when connecting + * @param benchRepo the benchmark repo + */ + public ServerMasterWebsocketServlet( + Dispatcher dispatcher, Serializer serializer, String runnerToken, BenchRepo benchRepo) { + this.dispatcher = dispatcher; + this.serializer = serializer; + this.runnerToken = runnerToken; + this.benchRepo = benchRepo; + } - @Override - public void configure(WebSocketServletFactory factory) { - factory.getPolicy().setMaxTextMessageSize(Integer.MAX_VALUE); - factory.setCreator((req, resp) -> { - String name = req.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName()); - String token = req.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName()); + @Override + public void configure(WebSocketServletFactory factory) { + factory.getPolicy().setMaxTextMessageSize(Integer.MAX_VALUE); + factory.setCreator( + (req, resp) -> { + String name = req.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName()); + String token = req.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName()); - if (name == null || !runnerToken.equals(token)) { - LOGGER.info("Runner from {} failed authentication!", req.getRemoteAddress()); - kickRunner(resp, RunnerDenyReason.TOKEN_INVALID); - return null; - } + if (name == null || !runnerToken.equals(token)) { + LOGGER.info("Runner from {} failed authentication!", req.getRemoteAddress()); + kickRunner(resp, RunnerDenyReason.TOKEN_INVALID); + return null; + } - Optional existingRunner = dispatcher.getTeleRunner(name); + Optional existingRunner = dispatcher.getTeleRunner(name); - TeleRunner myTeleRunner; - if (existingRunner.isPresent()) { - TeleRunner runner = existingRunner.get(); - if (runner.hasConnection()) { - kickRunner(resp, RunnerDenyReason.NAME_ALREADY_USED); - LOGGER.info( - "Kicked runner {} from {} as the name is taken!", name, req.getRemoteAddress() - ); - return null; - } - LOGGER.info("Reused runner connection for {} to ip {}!", name, req.getRemoteAddress()); - myTeleRunner = runner; - } else { - myTeleRunner = new TeleRunner(name, serializer, dispatcher, benchRepo); - LOGGER.info("Accepted runner {} from {}!", name, req.getRemoteAddress()); - } + TeleRunner myTeleRunner; + if (existingRunner.isPresent()) { + TeleRunner runner = existingRunner.get(); + if (runner.hasConnection()) { + kickRunner(resp, RunnerDenyReason.NAME_ALREADY_USED); + LOGGER.info( + "Kicked runner {} from {} as the name is taken!", name, req.getRemoteAddress()); + return null; + } + LOGGER.info("Reused runner connection for {} to ip {}!", name, req.getRemoteAddress()); + myTeleRunner = runner; + } else { + myTeleRunner = new TeleRunner(name, serializer, dispatcher, benchRepo); + LOGGER.info("Accepted runner {} from {}!", name, req.getRemoteAddress()); + } - // We synchronize on the runner object. A runner might join while the dispatcher cleans up - // disconnected runners. - // In this case we would remove the runner from the dispatchers teleRunners list but it - // would get a new connection and execute work ==> Bad - // So the dispatcher marks the runner as disposed and synchronizes on it. If the new runner - // connects while the dispatcher is removing the runner it will mark it as disposed. - // Either we come first, create a new connection and everything is well or the dispatcher - // comes first and disposes our runner object. - // In that case we detect that and create a new one... - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (myTeleRunner) { - if (myTeleRunner.isDisposed()) { - myTeleRunner = new TeleRunner(name, serializer, dispatcher, benchRepo); - LOGGER.info( - "Revived runner {} connecting from {} with new instance!", name, req.getRemoteAddress() - ); - } + // We synchronize on the runner object. A runner might join while the dispatcher cleans up + // disconnected runners. + // In this case we would remove the runner from the dispatchers teleRunners list but it + // would get a new connection and execute work ==> Bad + // So the dispatcher marks the runner as disposed and synchronizes on it. If the new + // runner + // connects while the dispatcher is removing the runner it will mark it as disposed. + // Either we come first, create a new connection and everything is well or the dispatcher + // comes first and disposes our runner object. + // In that case we detect that and create a new one... + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (myTeleRunner) { + if (myTeleRunner.isDisposed()) { + myTeleRunner = new TeleRunner(name, serializer, dispatcher, benchRepo); + LOGGER.info( + "Revived runner {} connecting from {} with new instance!", + name, + req.getRemoteAddress()); + } - return myTeleRunner.createConnection(); - } - }); - } + return myTeleRunner.createConnection(); + } + }); + } - private void kickRunner(ServletUpgradeResponse response, RunnerDenyReason reason) { - try { - response.addHeader( - RunnerConnectionHeader.DISCONNECT_DENY_REASON.getName(), - reason.getHeaderValue() - ); - response.sendError(reason.getCode(), reason.getMessage()); - } catch (IOException e) { - LOGGER.warn("Failed to kick runner", e); - } - } + private void kickRunner(ServletUpgradeResponse response, RunnerDenyReason reason) { + try { + response.addHeader( + RunnerConnectionHeader.DISCONNECT_DENY_REASON.getName(), reason.getHeaderValue()); + response.sendError(reason.getCode(), reason.getMessage()); + } catch (IOException e) { + LOGGER.warn("Failed to kick runner", e); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequester.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequester.java index 51f4edff0..749ffbc4b 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequester.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequester.java @@ -18,129 +18,123 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A thread that periodically asks a runner for its status. - */ +/** A thread that periodically asks a runner for its status. */ public class PeriodicStatusRequester { - private static final Logger LOGGER = LoggerFactory.getLogger(PeriodicStatusRequester.class); - - private final Thread worker; - private final TeleRunner teleRunner; - private final RunnerConnection connection; - private final StateMachine stateMachine; - private volatile boolean cancelled; - - public PeriodicStatusRequester(TeleRunner teleRunner, RunnerConnection connection, - StateMachine stateMachine) { - this.teleRunner = teleRunner; - this.connection = connection; - this.stateMachine = stateMachine; - - this.worker = new Thread(this::run, "PeriodicStatusRequester"); - this.worker.setDaemon(true); - } - - /** - * Starts the requester. - */ - public void start() { - this.worker.start(); - } - - private void run() { - while (!cancelled) { - try { - iteration(); - // Keep some distance to not overload the runner with too many requests - //noinspection BusyWait - Thread.sleep(Delays.REQUEST_STATUS_INTERVAL.toMillis()); - } catch (Exception e) { - if (cancelled) { - return; - } - LOGGER.error("Error communicating with runner or handling results", e); - try { - clearResults(); - } catch (Exception e2) { - LOGGER.error("Error telling runner to clear results", e2); - connection.close(StatusCode.INTERNAL_ERROR); - } - } - } - } - - private void iteration() throws ExecutionException { - try { - GetStatusReply statusReply = requestStatus(); - teleRunner.setRunnerInformation(statusReply); - - if (!statusReply.isResultAvailable()) { - return; - } - - GetResultReply requestResults = requestResults(); - UUID runId = requestResults.getRunId(); - - // The runner has a result - we don't know why :( Tell it to clear it and move on - if (teleRunner.getCurrentTask().isEmpty()) { - LOGGER.info( - "{} had a result but we don't know why. Clearing it.", teleRunner.getRunnerName() - ); - clearResults(); - return; - } - // The runner has a *different* result than we expected. Disconnect. - if (!teleRunner.getCurrentTask().get().getId().getId().equals(runId)) { - LOGGER.info( - "{} had a different result than we expected: {} instead of {}.", - teleRunner.getRunnerName(), runId, teleRunner.getCurrentTask().get().getId().getId() - ); - clearResults(); - connection.close(StatusCode.ILLEGAL_BEHAVIOUR); - return; - } - - LOGGER.info("Got results for run {} from {}", runId, teleRunner.getRunnerName()); - teleRunner.handleResults(requestResults); - - clearResults(); - } catch (InterruptedException | CancellationException ignored) { - } - } - - private GetStatusReply requestStatus() - throws InterruptedException, ExecutionException { - AwaitGetStatusReply statusReplyState = new AwaitGetStatusReply(teleRunner, connection); - - stateMachine.switchFromRestingState(statusReplyState); - connection.send(new GetStatus().asPacket(connection.getSerializer())); - - return statusReplyState.getReplyFuture().get(); - } - - private GetResultReply requestResults() - throws InterruptedException, ExecutionException { - AwaitGetResultReply resultReplyState = new AwaitGetResultReply(teleRunner, connection); - stateMachine.switchFromRestingState(resultReplyState); - connection.send(new GetResult().asPacket(connection.getSerializer())); - - return resultReplyState.getReplyFuture().get(); - } - - private void clearResults() throws InterruptedException, ExecutionException { - AwaitClearResultReply clearResultState = new AwaitClearResultReply(teleRunner, connection); - stateMachine.switchFromRestingState(clearResultState); - connection.send(new ClearResult().asPacket(connection.getSerializer())); - - clearResultState.getReplyFuture().get(); - } - - /** - * Stops this requestor and tears down the thread. This object can not be reused. - */ - public void cancel() { - cancelled = true; - worker.interrupt(); - } + private static final Logger LOGGER = LoggerFactory.getLogger(PeriodicStatusRequester.class); + + private final Thread worker; + private final TeleRunner teleRunner; + private final RunnerConnection connection; + private final StateMachine stateMachine; + private volatile boolean cancelled; + + public PeriodicStatusRequester( + TeleRunner teleRunner, + RunnerConnection connection, + StateMachine stateMachine) { + this.teleRunner = teleRunner; + this.connection = connection; + this.stateMachine = stateMachine; + + this.worker = new Thread(this::run, "PeriodicStatusRequester"); + this.worker.setDaemon(true); + } + + /** Starts the requester. */ + public void start() { + this.worker.start(); + } + + private void run() { + while (!cancelled) { + try { + iteration(); + // Keep some distance to not overload the runner with too many requests + //noinspection BusyWait + Thread.sleep(Delays.REQUEST_STATUS_INTERVAL.toMillis()); + } catch (Exception e) { + if (cancelled) { + return; + } + LOGGER.error("Error communicating with runner or handling results", e); + try { + clearResults(); + } catch (Exception e2) { + LOGGER.error("Error telling runner to clear results", e2); + connection.close(StatusCode.INTERNAL_ERROR); + } + } + } + } + + private void iteration() throws ExecutionException { + try { + GetStatusReply statusReply = requestStatus(); + teleRunner.setRunnerInformation(statusReply); + + if (!statusReply.isResultAvailable()) { + return; + } + + GetResultReply requestResults = requestResults(); + UUID runId = requestResults.getRunId(); + + // The runner has a result - we don't know why :( Tell it to clear it and move on + if (teleRunner.getCurrentTask().isEmpty()) { + LOGGER.info( + "{} had a result but we don't know why. Clearing it.", teleRunner.getRunnerName()); + clearResults(); + return; + } + // The runner has a *different* result than we expected. Disconnect. + if (!teleRunner.getCurrentTask().get().getId().getId().equals(runId)) { + LOGGER.info( + "{} had a different result than we expected: {} instead of {}.", + teleRunner.getRunnerName(), + runId, + teleRunner.getCurrentTask().get().getId().getId()); + clearResults(); + connection.close(StatusCode.ILLEGAL_BEHAVIOUR); + return; + } + + LOGGER.info("Got results for run {} from {}", runId, teleRunner.getRunnerName()); + teleRunner.handleResults(requestResults); + + clearResults(); + } catch (InterruptedException | CancellationException ignored) { + } + } + + private GetStatusReply requestStatus() throws InterruptedException, ExecutionException { + AwaitGetStatusReply statusReplyState = new AwaitGetStatusReply(teleRunner, connection); + + stateMachine.switchFromRestingState(statusReplyState); + connection.send(new GetStatus().asPacket(connection.getSerializer())); + + return statusReplyState.getReplyFuture().get(); + } + + private GetResultReply requestResults() throws InterruptedException, ExecutionException { + AwaitGetResultReply resultReplyState = new AwaitGetResultReply(teleRunner, connection); + stateMachine.switchFromRestingState(resultReplyState); + connection.send(new GetResult().asPacket(connection.getSerializer())); + + return resultReplyState.getReplyFuture().get(); + } + + private void clearResults() throws InterruptedException, ExecutionException { + AwaitClearResultReply clearResultState = new AwaitClearResultReply(teleRunner, connection); + stateMachine.switchFromRestingState(clearResultState); + connection.send(new ClearResult().asPacket(connection.getSerializer())); + + clearResultState.getReplyFuture().get(); + } + + /** Stops this requestor and tears down the thread. This object can not be reused. */ + public void cancel() { + cancelled = true; + worker.interrupt(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/RunnerConnection.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/RunnerConnection.java index 8800eb9f9..7f57d3ab5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/RunnerConnection.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/RunnerConnection.java @@ -28,214 +28,208 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A connection with a single runner. - */ -public class RunnerConnection implements WebSocketListener, WebSocketFrameListener, - HeartbeatWebsocket { - - private static final Logger LOGGER = LoggerFactory.getLogger(RunnerConnection.class); - - @Nullable - private Session session; - @Nullable - private ClosedBeforeConnect closedBeforeConnect; - - private final Serializer serializer; - private final StateMachine stateMachine; - private final AtomicReference lastPing; - private final PeriodicStatusRequester periodicStatusRequester; - private final List closeListeners; - private final HeartbeatHandler heartbeatHandler; - - public RunnerConnection(Serializer serializer, TeleRunner runner, - AtomicReference lastPing) { - this.serializer = serializer; - this.stateMachine = new StateMachine<>(new IdleState(runner, this)); - this.lastPing = lastPing; - this.closeListeners = new ArrayList<>(); - - this.periodicStatusRequester = new PeriodicStatusRequester(runner, this, stateMachine); - this.heartbeatHandler = new HeartbeatHandler(this); - - addCloseListener(this.periodicStatusRequester::cancel); - addCloseListener(this.heartbeatHandler::shutdown); - addCloseListener(() -> LOGGER.info("Connection to '{}' closed", runner.getRunnerName())); - } - - /** - * Adds a close listener. - * - * @param listener the close listener to add - */ - public void addCloseListener(Runnable listener) { - synchronized (this.closeListeners) { - this.closeListeners.add(listener); - } - } - - /** - * Returns the used serializer. - * - * @return the used serializer - */ - public Serializer getSerializer() { - return serializer; - } - - /** - * @return the internal state machine. - */ - public StateMachine getStateMachine() { - return stateMachine; - } - - /** - * Sends a packet to the runner. - *
- * If any error occurs, the connection is closed. - * - * @param packet the packet to send - * @throws IllegalArgumentException if the packet is not serializable - * @throws IllegalStateException if the connection is not yet connected - * @throws RuntimeIOException if an error occurred writing to the runner - */ - public void send(ClientBoundPacket packet) { - Optional serializedPacket = getSerializer().serialize(packet); - - if (serializedPacket.isEmpty()) { - close(StatusCode.INTERNAL_ERROR); - throw new IllegalArgumentException("Could not serialize packet " + packet); - } - if (session == null) { - close(StatusCode.INTERNAL_ERROR); - throw new IllegalStateException("Can not yet send packets"); - } - try { - session.getRemote().sendString(serializedPacket.get()); - } catch (IOException e) { - close(StatusCode.INTERNAL_ERROR); - throw new RuntimeIOException(e); - } - } - - - /** - * Streams binary data to the runner. - * - * @return an output stream that streams written data to the runner - */ - public OutputStream createBinaryOutputStream() { - return new TeleBinaryOutputStream(session); - } - - /** - * Closes the connecting, disconnecting the runner. - *
- *

If this is called before the session was opened, the session will be closed as soon as it - * is opened.

- * - * @param statusCode the status code to close it with - */ - public synchronized void close(StatusCode statusCode) { - if (session == null) { - closedBeforeConnect = new ClosedBeforeConnect(statusCode); - return; - } - - LOGGER.info("Closing connection to {} due to {}", session.getRemoteAddress(), statusCode); - - Timeout closeTimeout = Timeout.after(Delays.CLOSE_CONNECTION_TIMEOUT); - closeTimeout.getCompletionStage().thenRun(this::hardDisconnect); - closeTimeout.start(); - - // Abort the timeout if we closed properly - addCloseListener(closeTimeout::cancel); - - session.close(statusCode.getCode(), statusCode.getDescriptionAsReason()); - } - - private synchronized void hardDisconnect() { - try { - if (session != null) { - session.disconnect(); - } - } catch (IOException e) { - LOGGER.warn("Error in hard disconnect", e); - } - } - - @Override - public void onWebSocketFrame(Frame frame) { - if (frame.getType() == Type.PONG) { - lastPing.set(Instant.now()); - heartbeatHandler.onPong(); - } - } - - @Override - public void onWebSocketBinary(byte[] payload, int offset, int len) { - close(StatusCode.ILLEGAL_BINARY_PACKET); - } - - @Override - public void onWebSocketText(String message) { - stateMachine.changeCurrentState(state -> state.onText(message)); - } - - @Override - public void onWebSocketClose(int statusCode, String reason) { - callCloseListeners(); - } - - @Override - public void onWebSocketError(Throwable cause) { - callCloseListeners(); - } - - private void callCloseListeners() { - synchronized (this.closeListeners) { - this.closeListeners.forEach(Runnable::run); - } - } - - @Override - public synchronized void onWebSocketConnect(Session session) { - this.session = session; - if (closedBeforeConnect != null) { - close(closedBeforeConnect.statusCode); - } - - this.periodicStatusRequester.start(); - } - - @Override - public void onTimeoutDetected() { - close(StatusCode.PING_TIMEOUT); - } - - @Override - public boolean sendPing() { - if (session == null) { - return false; - } - try { - session.getRemote().sendPing(ByteBuffer.wrap( - Long.toString(System.currentTimeMillis()).getBytes() - )); - } catch (IOException e) { - LOGGER.warn("Could not send a ping", e); - return false; - } - return true; - } - - private static class ClosedBeforeConnect { - - final StatusCode statusCode; - - ClosedBeforeConnect(StatusCode statusCode) { - this.statusCode = statusCode; - } - } +/** A connection with a single runner. */ +public class RunnerConnection + implements WebSocketListener, WebSocketFrameListener, HeartbeatWebsocket { + + private static final Logger LOGGER = LoggerFactory.getLogger(RunnerConnection.class); + + @Nullable private Session session; + @Nullable private ClosedBeforeConnect closedBeforeConnect; + + private final Serializer serializer; + private final StateMachine stateMachine; + private final AtomicReference lastPing; + private final PeriodicStatusRequester periodicStatusRequester; + private final List closeListeners; + private final HeartbeatHandler heartbeatHandler; + + public RunnerConnection( + Serializer serializer, TeleRunner runner, AtomicReference lastPing) { + this.serializer = serializer; + this.stateMachine = new StateMachine<>(new IdleState(runner, this)); + this.lastPing = lastPing; + this.closeListeners = new ArrayList<>(); + + this.periodicStatusRequester = new PeriodicStatusRequester(runner, this, stateMachine); + this.heartbeatHandler = new HeartbeatHandler(this); + + addCloseListener(this.periodicStatusRequester::cancel); + addCloseListener(this.heartbeatHandler::shutdown); + addCloseListener(() -> LOGGER.info("Connection to '{}' closed", runner.getRunnerName())); + } + + /** + * Adds a close listener. + * + * @param listener the close listener to add + */ + public void addCloseListener(Runnable listener) { + synchronized (this.closeListeners) { + this.closeListeners.add(listener); + } + } + + /** + * Returns the used serializer. + * + * @return the used serializer + */ + public Serializer getSerializer() { + return serializer; + } + + /** + * @return the internal state machine. + */ + public StateMachine getStateMachine() { + return stateMachine; + } + + /** + * Sends a packet to the runner.
+ * If any error occurs, the connection is closed. + * + * @param packet the packet to send + * @throws IllegalArgumentException if the packet is not serializable + * @throws IllegalStateException if the connection is not yet connected + * @throws RuntimeIOException if an error occurred writing to the runner + */ + public void send(ClientBoundPacket packet) { + Optional serializedPacket = getSerializer().serialize(packet); + + if (serializedPacket.isEmpty()) { + close(StatusCode.INTERNAL_ERROR); + throw new IllegalArgumentException("Could not serialize packet " + packet); + } + if (session == null) { + close(StatusCode.INTERNAL_ERROR); + throw new IllegalStateException("Can not yet send packets"); + } + try { + session.getRemote().sendString(serializedPacket.get()); + } catch (IOException e) { + close(StatusCode.INTERNAL_ERROR); + throw new RuntimeIOException(e); + } + } + + /** + * Streams binary data to the runner. + * + * @return an output stream that streams written data to the runner + */ + public OutputStream createBinaryOutputStream() { + return new TeleBinaryOutputStream(session); + } + + /** + * Closes the connecting, disconnecting the runner.
+ * + *

If this is called before the session was opened, the session will be closed as soon as it is + * opened. + * + * @param statusCode the status code to close it with + */ + public synchronized void close(StatusCode statusCode) { + if (session == null) { + closedBeforeConnect = new ClosedBeforeConnect(statusCode); + return; + } + + LOGGER.info("Closing connection to {} due to {}", session.getRemoteAddress(), statusCode); + + Timeout closeTimeout = Timeout.after(Delays.CLOSE_CONNECTION_TIMEOUT); + closeTimeout.getCompletionStage().thenRun(this::hardDisconnect); + closeTimeout.start(); + + // Abort the timeout if we closed properly + addCloseListener(closeTimeout::cancel); + + session.close(statusCode.getCode(), statusCode.getDescriptionAsReason()); + } + + private synchronized void hardDisconnect() { + try { + if (session != null) { + session.disconnect(); + } + } catch (IOException e) { + LOGGER.warn("Error in hard disconnect", e); + } + } + + @Override + public void onWebSocketFrame(Frame frame) { + if (frame.getType() == Type.PONG) { + lastPing.set(Instant.now()); + heartbeatHandler.onPong(); + } + } + + @Override + public void onWebSocketBinary(byte[] payload, int offset, int len) { + close(StatusCode.ILLEGAL_BINARY_PACKET); + } + + @Override + public void onWebSocketText(String message) { + stateMachine.changeCurrentState(state -> state.onText(message)); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + callCloseListeners(); + } + + @Override + public void onWebSocketError(Throwable cause) { + callCloseListeners(); + } + + private void callCloseListeners() { + synchronized (this.closeListeners) { + this.closeListeners.forEach(Runnable::run); + } + } + + @Override + public synchronized void onWebSocketConnect(Session session) { + this.session = session; + if (closedBeforeConnect != null) { + close(closedBeforeConnect.statusCode); + } + + this.periodicStatusRequester.start(); + } + + @Override + public void onTimeoutDetected() { + close(StatusCode.PING_TIMEOUT); + } + + @Override + public boolean sendPing() { + if (session == null) { + return false; + } + try { + session + .getRemote() + .sendPing(ByteBuffer.wrap(Long.toString(System.currentTimeMillis()).getBytes())); + } catch (IOException e) { + LOGGER.warn("Could not send a ping", e); + return false; + } + return true; + } + + private static class ClosedBeforeConnect { + + final StatusCode statusCode; + + ClosedBeforeConnect(StatusCode statusCode) { + this.statusCode = statusCode; + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleBinaryOutputStream.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleBinaryOutputStream.java index a5dcdd6d8..522a1f8e2 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleBinaryOutputStream.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleBinaryOutputStream.java @@ -5,41 +5,38 @@ import java.nio.ByteBuffer; import org.eclipse.jetty.websocket.api.Session; -/** - * A binary output stream streaming data to the runner. - */ +/** A binary output stream streaming data to the runner. */ class TeleBinaryOutputStream extends OutputStream { - private static final int DEFAULT_BUFFER_SIZE = 1024; // 1 MB - - private boolean closed; - private ByteBuffer buffer; - private final Session session; - - public TeleBinaryOutputStream(Session session) { - this.session = session; - this.buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); - } - - @Override - public void write(int b) throws IOException { - if (!buffer.hasRemaining()) { - buffer.rewind(); - session.getRemote().sendPartialBytes(buffer, false); - buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); - } - buffer.put((byte) b); - } - - @Override - public void close() throws IOException { - if (closed) { - return; - } - buffer.limit(buffer.position()); - buffer.rewind(); - session.getRemote().sendPartialBytes(buffer, true); - closed = true; - } - + private static final int DEFAULT_BUFFER_SIZE = 1024; // 1 MB + + private boolean closed; + private ByteBuffer buffer; + private final Session session; + + public TeleBinaryOutputStream(Session session) { + this.session = session; + this.buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + } + + @Override + public void write(int b) throws IOException { + if (!buffer.hasRemaining()) { + buffer.rewind(); + session.getRemote().sendPartialBytes(buffer, false); + buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + } + buffer.put((byte) b); + } + + @Override + public void close() throws IOException { + if (closed) { + return; + } + buffer.limit(buffer.position()); + buffer.rewind(); + session.getRemote().sendPartialBytes(buffer, true); + closed = true; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunner.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunner.java index 8b5edb3c3..be2426268 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunner.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunner.java @@ -38,396 +38,376 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Server side class for a runner. - */ +/** Server side class for a runner. */ public class TeleRunner { - private static final Logger LOGGER = LoggerFactory.getLogger(TeleRunner.class); - /** - * Completed tasks are cached so we can continue to show output lines in the frontend even if the - * task has finished. This variable controls *how many* are ached. - */ - private static final int MAX_CACHED_COMPLETED_TASKS = 2; - - private final AtomicReference runnerInformation; - private final Queue lastResults; - private final String runnerName; - private final Serializer serializer; - private final Dispatcher dispatcher; - private final AtomicReference myCurrentTask; - private final AtomicReference workingSince; - private final BenchRepo benchRepo; - private final AtomicReference lastPing; - - private volatile boolean disposed; - - private RunnerConnection connection; - - public TeleRunner(String runnerName, Serializer serializer, Dispatcher dispatcher, - BenchRepo benchRepo) { - this.runnerName = runnerName; - this.serializer = serializer; - this.dispatcher = dispatcher; - this.benchRepo = benchRepo; - this.runnerInformation = new AtomicReference<>(); - this.myCurrentTask = new AtomicReference<>(); - this.workingSince = new AtomicReference<>(); - this.lastPing = new AtomicReference<>(); - this.lastResults = new ArrayDeque<>(); - } - - /** - * Creates a new connection, if none exists. - * - * @return the created connection - * @throws IllegalStateException if this runner already has a connection - */ - public synchronized RunnerConnection createConnection() throws IllegalStateException { - if (connection != null) { - throw new IllegalStateException("I already have a connection"); - } - if (disposed) { - throw new IllegalStateException("I am disposed"); - } - connection = new RunnerConnection(serializer, this, lastPing); - connection.addCloseListener(this::disposeConnection); - - lastPing.set(Instant.now()); - - return connection; - } - - private void disposeConnection() { - synchronized (this) { - this.connection = null; - } - } - - /** - * @return true if this runner has an active connection. - */ - public synchronized boolean hasConnection() { - return connection != null; - } - - /** - * Returns the unique name for this runner. - * - * @return the runner name - */ - public String getRunnerName() { - return runnerName; - } - - /** - * Sets the runner information. - * - * @param reply the reply - */ - public void setRunnerInformation(GetStatusReply reply) { - if (runnerInformation.get() == null) { - LOGGER.debug("Passing runner '{}' on to dispatcher", getRunnerName()); - dispatcher.addRunner(this); - } - runnerInformation.set(reply); - - if (reply.getRunId().isPresent()) { - TaskId taskId = new TaskId(reply.getRunId().get()); - boolean taskInProgress = dispatcher.getQueue().isTaskInProgress(taskId); - // Task is no longer in the queue or no longer in progress - if (!taskInProgress) { - abort(); - } - } - - } - - /** - * @return the current task of this runner, if any - */ - public Optional getCurrentTask() { - return Optional.ofNullable(myCurrentTask.get()); - } - - /** - * @return the time the last ping was received - */ - public Instant getLastPing() { - return lastPing.get(); - } - - /** - * Disposes this TeleRunner. It can not be used again. - */ - public synchronized void dispose() { - this.disposed = true; - if (hasConnection()) { - LOGGER.warn("Had a connection when I was disposed"); - this.connection.close(StatusCode.INTERNAL_ERROR); - } - getCurrentTask().ifPresent(task -> dispatcher.getQueue().abortTask(task.getId())); - } - - /** - * @return true if this TeleRunner is disposed and can not be used again - */ - public synchronized boolean isDisposed() { - return disposed; - } - - /** - * @return the runner information. This must always be non-null from the moment the runner is - * registered to the dispatcher. The value might be outdated. - */ - public KnownRunner getRunnerInformation() { - GetStatusReply reply = runnerInformation.get(); - - synchronized (lastResults) { - return new KnownRunner( - getRunnerName(), - reply.getInfo(), - reply.getVersionHash().orElse(null), - reply.getStatus(), - myCurrentTask.get(), - !hasConnection(), - workingSince.get(), - reply.getLastOutputLines().orElse(null), - new ArrayList<>(lastResults) - ); - } - } - - /** - * Aborts the current benchmark. This is a very rudimentary implementation at the moment. - *
- * The abort will do nothing, if - *

    - *
  • The runner is not connected
  • - *
  • The send fails
  • - *
  • The runner reconnects and has results for that run
  • - *
  • The server sends a get_status before the abort and the runner has results
  • - *
- * These limitations might be lifted in the future. - */ - private void abort() { - myCurrentTask.set(null); - workingSince.set(null); - - if (!hasConnection()) { - LOGGER.info( - "Tried to abort commit but was not connected with a runner: {}", getRunnerName() - ); - return; - } - - try { - connection.getStateMachine() - .switchFromRestingState(new AwaitAbortRunReply(this, connection)); - connection.send(new AbortRun().asPacket(serializer)); - } catch (InterruptedException e) { - LOGGER.warn("Abort failed, I was interrupted {}", getRunnerName()); - } - } - - /** - * Handles a result, saving the data in the DB if the task matches the stored one. - * - * @param resultReply the results - */ - public void handleResults(GetResultReply resultReply) { - workingSince.set(null); - - Task task = myCurrentTask.get(); - if (task == null) { - LOGGER.warn("{} has no task but got results :(", getRunnerName()); - // Somehow we have no task but a result, retry that commit. This should not happen, but if it - // does err on the side of caution. Retry that task if possible, better to benchmark twice - // than never - dispatcher.getQueue().abortTask(new TaskId(resultReply.getRunId())); - return; - } - - synchronized (lastResults) { - lastResults.offer(new CompletedTask( - new TaskId(resultReply.getRunId()), - runnerInformation.get().getLastOutputLines().orElse(null) - )); - - while (lastResults.size() > MAX_CACHED_COMPLETED_TASKS) { - lastResults.poll(); - } - } - - NewRun run; - if (resultReply.isSuccess()) { - // the benchmark script exited normally - run = handleSuccessful(resultReply, task); - } else { - // the benchmark script misbehaved - run = handleUnsuccessful(resultReply, task); - } - dispatcher.completeTask(run); - - myCurrentTask.set(null); - } - - private NewRun handleUnsuccessful(GetResultReply resultReply, Task task) { - RunBuilder builder = RunBuilder.failed( - task, - getRunnerName(), - getRunnerInformation().getInformation(), - resultReply.getStartTime(), - resultReply.getStopTime(), - resultReply.getError().orElseThrow(), - RunErrorType.BENCH_SCRIPT_ERROR - ); - return builder.build(); - } - - private NewRun handleSuccessful(GetResultReply resultReply, Task task) { - Result result = resultReply.getResult().orElseThrow(); - - // Global error - if (result.getError().isPresent()) { - return RunBuilder.failed( - task, - getRunnerName(), - getRunnerInformation().getInformation(), - resultReply.getStartTime(), - resultReply.getStopTime(), - result.getError().get(), - RunErrorType.BENCH_SCRIPT_ERROR - ).build(); - } - - // No global error, but maybe individual ones failed - RunBuilder builder = RunBuilder.successful( - task, - getRunnerName(), - getRunnerInformation().getInformation(), - resultReply.getStartTime(), - resultReply.getStopTime() - ); - - // That get is okay, we check for `getError().isPresent() - //noinspection OptionalGetWithoutIsPresent - for (Benchmark benchmark : result.getBenchmarks().get()) { - for (Metric metric : benchmark.getMetrics()) { - Dimension name = new Dimension(benchmark.getName(), metric.getName()); - if (metric.getError().isPresent()) { - builder.addFailedMeasurement( - name, - metric.getUnit().map(Unit::new).orElse(null), - metric.getInterpretation().map(Interpretation::fromSharedRepresentation).orElse(null), - metric.getError().get() - ); - } else { - // That get is okay, we check for metric.getError().isPresent() - //noinspection OptionalGetWithoutIsPresent - builder.addSuccessfulMeasurement( - name, - metric.getUnit().map(Unit::new).orElse(null), - metric.getInterpretation().map(Interpretation::fromSharedRepresentation).orElse(null), - metric.getValues().get() - ); - } - } - } - - return builder.build(); - } - - /** - * Sends a {@link RequestRunReply} and any needed TARs. - */ - public void prepareAndSendWork() { - LOGGER.debug("Runner {} asks for work", getRunnerName()); - - Optional benchRepoHash = benchRepo.getCurrentHash().map(CommitHash::getHash); - - if (benchRepoHash.isEmpty()) { - LOGGER.info( - "Benchmark repo has no hash (yet?), dispatching temporarily stopped for {}.", - getRunnerName() - ); - return; - } - - Optional workOptional = Optional.ofNullable(myCurrentTask.get()) - .or(() -> dispatcher.getWork(this)); - - if (workOptional.isEmpty()) { - LOGGER.debug("Dispatcher gave me no work for {}", getRunnerName()); - connection.send( - new RequestRunReply(false, null, false, null).asPacket(serializer) - ); - return; - } - Task task = workOptional.get(); - myCurrentTask.set(task); - workingSince.set(Instant.now()); - - boolean benchRepoUpToDate = runnerInformation.get().getBenchHash() - .map(it -> it.equals(benchRepoHash.get())) - .orElse(false); - - LOGGER.info("Sending {} to runner {}", task.getId().getId(), getRunnerName()); - - connection.send( - new RequestRunReply( - !benchRepoUpToDate, - benchRepoUpToDate ? null : benchRepoHash.get(), - true, - task.getId().getId() - ) - .asPacket(serializer) - ); - - if (!benchRepoUpToDate) { - handleBinaryTransfer(task, benchRepo::transfer); - } - - handleBinaryTransfer( - task, - outputStream -> dispatcher.getQueue().transferTask(task.getId(), outputStream) - ); - } - - private void handleBinaryTransfer(Task task, TransferConsumer consumer) { - try (OutputStream outputStream = connection.createBinaryOutputStream()) { - consumer.accept(outputStream); - } catch (TarRetrieveException e) { - LOGGER.info( - "Failed to transfer repo to runner " + getRunnerName() + ": Archiving failed", e - ); - // This task is corrupted, we can not benchmark it. - dispatcher.completeTask(tarRetrieveFailed(task, Instant.now(), e)); - } catch (TarTransferException | IOException | NoSuchTaskException e) { - LOGGER.info( - "Failed to transfer repo to runner " + getRunnerName() + ": Sending failed", e - ); - dispatcher.getQueue().abortTask(task.getId()); - connection.close(StatusCode.TRANSFER_FAILED); - } - } - - private NewRun tarRetrieveFailed(Task task, Instant start, TarRetrieveException exception) { - return RunBuilder.failed( - task, - getRunnerName(), - getRunnerInformation().getInformation(), - start, - Instant.now(), - "Archiving failed. Error:\n " + ExceptionHelper.getStackTrace(exception), - RunErrorType.VELCOM_ERROR - ).build(); - } - - private interface TransferConsumer { - - void accept(OutputStream outputStream) - throws TarRetrieveException, TarTransferException, IOException, NoSuchTaskException; - } - + private static final Logger LOGGER = LoggerFactory.getLogger(TeleRunner.class); + + /** + * Completed tasks are cached so we can continue to show output lines in the frontend even if the + * task has finished. This variable controls *how many* are ached. + */ + private static final int MAX_CACHED_COMPLETED_TASKS = 2; + + private final AtomicReference runnerInformation; + private final Queue lastResults; + private final String runnerName; + private final Serializer serializer; + private final Dispatcher dispatcher; + private final AtomicReference myCurrentTask; + private final AtomicReference workingSince; + private final BenchRepo benchRepo; + private final AtomicReference lastPing; + + private volatile boolean disposed; + + private RunnerConnection connection; + + public TeleRunner( + String runnerName, Serializer serializer, Dispatcher dispatcher, BenchRepo benchRepo) { + this.runnerName = runnerName; + this.serializer = serializer; + this.dispatcher = dispatcher; + this.benchRepo = benchRepo; + this.runnerInformation = new AtomicReference<>(); + this.myCurrentTask = new AtomicReference<>(); + this.workingSince = new AtomicReference<>(); + this.lastPing = new AtomicReference<>(); + this.lastResults = new ArrayDeque<>(); + } + + /** + * Creates a new connection, if none exists. + * + * @return the created connection + * @throws IllegalStateException if this runner already has a connection + */ + public synchronized RunnerConnection createConnection() throws IllegalStateException { + if (connection != null) { + throw new IllegalStateException("I already have a connection"); + } + if (disposed) { + throw new IllegalStateException("I am disposed"); + } + connection = new RunnerConnection(serializer, this, lastPing); + connection.addCloseListener(this::disposeConnection); + + lastPing.set(Instant.now()); + + return connection; + } + + private void disposeConnection() { + synchronized (this) { + this.connection = null; + } + } + + /** + * @return true if this runner has an active connection. + */ + public synchronized boolean hasConnection() { + return connection != null; + } + + /** + * Returns the unique name for this runner. + * + * @return the runner name + */ + public String getRunnerName() { + return runnerName; + } + + /** + * Sets the runner information. + * + * @param reply the reply + */ + public void setRunnerInformation(GetStatusReply reply) { + if (runnerInformation.get() == null) { + LOGGER.debug("Passing runner '{}' on to dispatcher", getRunnerName()); + dispatcher.addRunner(this); + } + runnerInformation.set(reply); + + if (reply.getRunId().isPresent()) { + TaskId taskId = new TaskId(reply.getRunId().get()); + boolean taskInProgress = dispatcher.getQueue().isTaskInProgress(taskId); + // Task is no longer in the queue or no longer in progress + if (!taskInProgress) { + abort(); + } + } + } + + /** + * @return the current task of this runner, if any + */ + public Optional getCurrentTask() { + return Optional.ofNullable(myCurrentTask.get()); + } + + /** + * @return the time the last ping was received + */ + public Instant getLastPing() { + return lastPing.get(); + } + + /** Disposes this TeleRunner. It can not be used again. */ + public synchronized void dispose() { + this.disposed = true; + if (hasConnection()) { + LOGGER.warn("Had a connection when I was disposed"); + this.connection.close(StatusCode.INTERNAL_ERROR); + } + getCurrentTask().ifPresent(task -> dispatcher.getQueue().abortTask(task.getId())); + } + + /** + * @return true if this TeleRunner is disposed and can not be used again + */ + public synchronized boolean isDisposed() { + return disposed; + } + + /** + * @return the runner information. This must always be non-null from the moment the runner is + * registered to the dispatcher. The value might be outdated. + */ + public KnownRunner getRunnerInformation() { + GetStatusReply reply = runnerInformation.get(); + + synchronized (lastResults) { + return new KnownRunner( + getRunnerName(), + reply.getInfo(), + reply.getVersionHash().orElse(null), + reply.getStatus(), + myCurrentTask.get(), + !hasConnection(), + workingSince.get(), + reply.getLastOutputLines().orElse(null), + new ArrayList<>(lastResults)); + } + } + + /** + * Aborts the current benchmark. This is a very rudimentary implementation at the moment.
+ * The abort will do nothing, if + * + *
    + *
  • The runner is not connected + *
  • The send fails + *
  • The runner reconnects and has results for that run + *
  • The server sends a get_status before the abort and the runner has results + *
+ * + * These limitations might be lifted in the future. + */ + private void abort() { + myCurrentTask.set(null); + workingSince.set(null); + + if (!hasConnection()) { + LOGGER.info("Tried to abort commit but was not connected with a runner: {}", getRunnerName()); + return; + } + + try { + connection.getStateMachine().switchFromRestingState(new AwaitAbortRunReply(this, connection)); + connection.send(new AbortRun().asPacket(serializer)); + } catch (InterruptedException e) { + LOGGER.warn("Abort failed, I was interrupted {}", getRunnerName()); + } + } + + /** + * Handles a result, saving the data in the DB if the task matches the stored one. + * + * @param resultReply the results + */ + public void handleResults(GetResultReply resultReply) { + workingSince.set(null); + + Task task = myCurrentTask.get(); + if (task == null) { + LOGGER.warn("{} has no task but got results :(", getRunnerName()); + // Somehow we have no task but a result, retry that commit. This should not happen, but if it + // does err on the side of caution. Retry that task if possible, better to benchmark twice + // than never + dispatcher.getQueue().abortTask(new TaskId(resultReply.getRunId())); + return; + } + + synchronized (lastResults) { + lastResults.offer( + new CompletedTask( + new TaskId(resultReply.getRunId()), + runnerInformation.get().getLastOutputLines().orElse(null))); + + while (lastResults.size() > MAX_CACHED_COMPLETED_TASKS) { + lastResults.poll(); + } + } + + NewRun run; + if (resultReply.isSuccess()) { + // the benchmark script exited normally + run = handleSuccessful(resultReply, task); + } else { + // the benchmark script misbehaved + run = handleUnsuccessful(resultReply, task); + } + dispatcher.completeTask(run); + + myCurrentTask.set(null); + } + + private NewRun handleUnsuccessful(GetResultReply resultReply, Task task) { + RunBuilder builder = + RunBuilder.failed( + task, + getRunnerName(), + getRunnerInformation().getInformation(), + resultReply.getStartTime(), + resultReply.getStopTime(), + resultReply.getError().orElseThrow(), + RunErrorType.BENCH_SCRIPT_ERROR); + return builder.build(); + } + + private NewRun handleSuccessful(GetResultReply resultReply, Task task) { + Result result = resultReply.getResult().orElseThrow(); + + // Global error + if (result.getError().isPresent()) { + return RunBuilder.failed( + task, + getRunnerName(), + getRunnerInformation().getInformation(), + resultReply.getStartTime(), + resultReply.getStopTime(), + result.getError().get(), + RunErrorType.BENCH_SCRIPT_ERROR) + .build(); + } + + // No global error, but maybe individual ones failed + RunBuilder builder = + RunBuilder.successful( + task, + getRunnerName(), + getRunnerInformation().getInformation(), + resultReply.getStartTime(), + resultReply.getStopTime()); + + // That get is okay, we check for `getError().isPresent() + //noinspection OptionalGetWithoutIsPresent + for (Benchmark benchmark : result.getBenchmarks().get()) { + for (Metric metric : benchmark.getMetrics()) { + Dimension name = new Dimension(benchmark.getName(), metric.getName()); + if (metric.getError().isPresent()) { + builder.addFailedMeasurement( + name, + metric.getUnit().map(Unit::new).orElse(null), + metric.getInterpretation().map(Interpretation::fromSharedRepresentation).orElse(null), + metric.getError().get()); + } else { + // That get is okay, we check for metric.getError().isPresent() + //noinspection OptionalGetWithoutIsPresent + builder.addSuccessfulMeasurement( + name, + metric.getUnit().map(Unit::new).orElse(null), + metric.getInterpretation().map(Interpretation::fromSharedRepresentation).orElse(null), + metric.getValues().get()); + } + } + } + + return builder.build(); + } + + /** Sends a {@link RequestRunReply} and any needed TARs. */ + public void prepareAndSendWork() { + LOGGER.debug("Runner {} asks for work", getRunnerName()); + + Optional benchRepoHash = benchRepo.getCurrentHash().map(CommitHash::getHash); + + if (benchRepoHash.isEmpty()) { + LOGGER.info( + "Benchmark repo has no hash (yet?), dispatching temporarily stopped for {}.", + getRunnerName()); + return; + } + + Optional workOptional = + Optional.ofNullable(myCurrentTask.get()).or(() -> dispatcher.getWork(this)); + + if (workOptional.isEmpty()) { + LOGGER.debug("Dispatcher gave me no work for {}", getRunnerName()); + connection.send(new RequestRunReply(false, null, false, null).asPacket(serializer)); + return; + } + Task task = workOptional.get(); + myCurrentTask.set(task); + workingSince.set(Instant.now()); + + boolean benchRepoUpToDate = + runnerInformation + .get() + .getBenchHash() + .map(it -> it.equals(benchRepoHash.get())) + .orElse(false); + + LOGGER.info("Sending {} to runner {}", task.getId().getId(), getRunnerName()); + + connection.send( + new RequestRunReply( + !benchRepoUpToDate, + benchRepoUpToDate ? null : benchRepoHash.get(), + true, + task.getId().getId()) + .asPacket(serializer)); + + if (!benchRepoUpToDate) { + handleBinaryTransfer(task, benchRepo::transfer); + } + + handleBinaryTransfer( + task, outputStream -> dispatcher.getQueue().transferTask(task.getId(), outputStream)); + } + + private void handleBinaryTransfer(Task task, TransferConsumer consumer) { + try (OutputStream outputStream = connection.createBinaryOutputStream()) { + consumer.accept(outputStream); + } catch (TarRetrieveException e) { + LOGGER.info("Failed to transfer repo to runner " + getRunnerName() + ": Archiving failed", e); + // This task is corrupted, we can not benchmark it. + dispatcher.completeTask(tarRetrieveFailed(task, Instant.now(), e)); + } catch (TarTransferException | IOException | NoSuchTaskException e) { + LOGGER.info("Failed to transfer repo to runner " + getRunnerName() + ": Sending failed", e); + dispatcher.getQueue().abortTask(task.getId()); + connection.close(StatusCode.TRANSFER_FAILED); + } + } + + private NewRun tarRetrieveFailed(Task task, Instant start, TarRetrieveException exception) { + return RunBuilder.failed( + task, + getRunnerName(), + getRunnerInformation().getInformation(), + start, + Instant.now(), + "Archiving failed. Error:\n " + ExceptionHelper.getStackTrace(exception), + RunErrorType.VELCOM_ERROR) + .build(); + } + + private interface TransferConsumer { + + void accept(OutputStream outputStream) + throws TarRetrieveException, TarTransferException, IOException, NoSuchTaskException; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitAbortRunReply.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitAbortRunReply.java index 0745b04b5..aec203b8c 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitAbortRunReply.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitAbortRunReply.java @@ -7,21 +7,25 @@ import de.aaaaaaah.velcom.shared.protocol.serialization.serverbound.ServerBoundPacketType; import java.util.Optional; -/** - * A state waiting for a reply to the GetResult message. - */ +/** A state waiting for a reply to the GetResult message. */ public class AwaitAbortRunReply extends TimeoutState { - public AwaitAbortRunReply(TeleRunner runner, RunnerConnection connection) { - super(runner, connection); - } + public AwaitAbortRunReply(TeleRunner runner, RunnerConnection connection) { + super(runner, connection); + } - @Override - protected Optional onPacket(ServerBoundPacket packet) { - return super.onPacket(packet).or(() -> Optional.of(packet) - .filter(it -> it.getType() == ServerBoundPacketType.ABORT_RUN_REPLY) - .flatMap(it -> connection.getSerializer().deserialize(it.getData(), AbortRunReply.class)) - .map(reply -> new IdleState(runner, connection)) - ); - } + @Override + protected Optional onPacket(ServerBoundPacket packet) { + return super.onPacket(packet) + .or( + () -> + Optional.of(packet) + .filter(it -> it.getType() == ServerBoundPacketType.ABORT_RUN_REPLY) + .flatMap( + it -> + connection + .getSerializer() + .deserialize(it.getData(), AbortRunReply.class)) + .map(reply -> new IdleState(runner, connection))); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitClearResultReply.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitClearResultReply.java index 8f83723bc..addec906e 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitClearResultReply.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitClearResultReply.java @@ -8,42 +8,47 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; -/** - * A state waiting for a reply to the GetResult message. - */ +/** A state waiting for a reply to the GetResult message. */ public class AwaitClearResultReply extends TimeoutState { - private final CompletableFuture replyFuture; - - public AwaitClearResultReply(TeleRunner runner, RunnerConnection connection) { - super(runner, connection); - - this.replyFuture = new CompletableFuture<>(); - } - - public CompletableFuture getReplyFuture() { - return replyFuture; - } - - @Override - public void onExit() { - super.onExit(); - - if (!replyFuture.isDone()) { - replyFuture.cancel(true); - } - } - - @Override - protected Optional onPacket(ServerBoundPacket packet) { - return super.onPacket(packet).or(() -> Optional.of(packet) - .filter(it -> it.getType() == ServerBoundPacketType.CLEAR_RESULT_REPLY) - .flatMap(it -> connection.getSerializer().deserialize(it.getData(), ClearResultReply.class)) - .map(reply -> { - replyFuture.complete(null); - - return new IdleState(runner, connection); - }) - ); - } + private final CompletableFuture replyFuture; + + public AwaitClearResultReply(TeleRunner runner, RunnerConnection connection) { + super(runner, connection); + + this.replyFuture = new CompletableFuture<>(); + } + + public CompletableFuture getReplyFuture() { + return replyFuture; + } + + @Override + public void onExit() { + super.onExit(); + + if (!replyFuture.isDone()) { + replyFuture.cancel(true); + } + } + + @Override + protected Optional onPacket(ServerBoundPacket packet) { + return super.onPacket(packet) + .or( + () -> + Optional.of(packet) + .filter(it -> it.getType() == ServerBoundPacketType.CLEAR_RESULT_REPLY) + .flatMap( + it -> + connection + .getSerializer() + .deserialize(it.getData(), ClearResultReply.class)) + .map( + reply -> { + replyFuture.complete(null); + + return new IdleState(runner, connection); + })); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetResultReply.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetResultReply.java index 83af3a777..ba6856b67 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetResultReply.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetResultReply.java @@ -8,42 +8,47 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; -/** - * A state waiting for a reply to the GetResult message. - */ +/** A state waiting for a reply to the GetResult message. */ public class AwaitGetResultReply extends TimeoutState { - private final CompletableFuture replyFuture; - - public AwaitGetResultReply(TeleRunner runner, RunnerConnection connection) { - super(runner, connection); - - this.replyFuture = new CompletableFuture<>(); - } - - public CompletableFuture getReplyFuture() { - return replyFuture; - } - - @Override - public void onExit() { - super.onExit(); - - if (!replyFuture.isDone()) { - replyFuture.cancel(true); - } - } - - @Override - protected Optional onPacket(ServerBoundPacket packet) { - return super.onPacket(packet).or(() -> Optional.of(packet) - .filter(it -> it.getType() == ServerBoundPacketType.GET_RESULT_REPLY) - .flatMap(it -> connection.getSerializer().deserialize(it.getData(), GetResultReply.class)) - .map(reply -> { - replyFuture.complete(reply); - - return new IdleState(runner, connection); - }) - ); - } + private final CompletableFuture replyFuture; + + public AwaitGetResultReply(TeleRunner runner, RunnerConnection connection) { + super(runner, connection); + + this.replyFuture = new CompletableFuture<>(); + } + + public CompletableFuture getReplyFuture() { + return replyFuture; + } + + @Override + public void onExit() { + super.onExit(); + + if (!replyFuture.isDone()) { + replyFuture.cancel(true); + } + } + + @Override + protected Optional onPacket(ServerBoundPacket packet) { + return super.onPacket(packet) + .or( + () -> + Optional.of(packet) + .filter(it -> it.getType() == ServerBoundPacketType.GET_RESULT_REPLY) + .flatMap( + it -> + connection + .getSerializer() + .deserialize(it.getData(), GetResultReply.class)) + .map( + reply -> { + replyFuture.complete(reply); + + return new IdleState(runner, connection); + })); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetStatusReply.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetStatusReply.java index 6432d02ba..ba2f0d23b 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetStatusReply.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/AwaitGetStatusReply.java @@ -8,45 +8,50 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; -/** - * A state waiting for a reply to the GetStatus message. - */ +/** A state waiting for a reply to the GetStatus message. */ public class AwaitGetStatusReply extends TimeoutState { - private final CompletableFuture replyFuture; - - public AwaitGetStatusReply(TeleRunner runner, RunnerConnection connection) { - super(runner, connection); - - this.replyFuture = new CompletableFuture<>(); - } - - /** - * @return the reply future. - */ - public CompletableFuture getReplyFuture() { - return replyFuture; - } - - @Override - public void onExit() { - super.onExit(); - - if (!replyFuture.isDone()) { - replyFuture.cancel(true); - } - } - - @Override - protected Optional onPacket(ServerBoundPacket packet) { - return super.onPacket(packet).or(() -> Optional.of(packet) - .filter(it -> it.getType() == ServerBoundPacketType.GET_STATUS_REPLY) - .flatMap(it -> connection.getSerializer().deserialize(it.getData(), GetStatusReply.class)) - .map(reply -> { - replyFuture.complete(reply); - - return new IdleState(runner, connection); - }) - ); - } + private final CompletableFuture replyFuture; + + public AwaitGetStatusReply(TeleRunner runner, RunnerConnection connection) { + super(runner, connection); + + this.replyFuture = new CompletableFuture<>(); + } + + /** + * @return the reply future. + */ + public CompletableFuture getReplyFuture() { + return replyFuture; + } + + @Override + public void onExit() { + super.onExit(); + + if (!replyFuture.isDone()) { + replyFuture.cancel(true); + } + } + + @Override + protected Optional onPacket(ServerBoundPacket packet) { + return super.onPacket(packet) + .or( + () -> + Optional.of(packet) + .filter(it -> it.getType() == ServerBoundPacketType.GET_STATUS_REPLY) + .flatMap( + it -> + connection + .getSerializer() + .deserialize(it.getData(), GetStatusReply.class)) + .map( + reply -> { + replyFuture.complete(reply); + + return new IdleState(runner, connection); + })); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/IdleState.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/IdleState.java index 2867b5eb3..ed09eb57a 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/IdleState.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/IdleState.java @@ -3,17 +3,15 @@ import de.aaaaaaah.velcom.backend.runner.single.RunnerConnection; import de.aaaaaaah.velcom.backend.runner.single.TeleRunner; -/** - * The tele-runner is in an idle state. - */ +/** The tele-runner is in an idle state. */ public class IdleState extends TeleRunnerState { - public IdleState(TeleRunner runner, RunnerConnection connection) { - super(runner, connection); - } + public IdleState(TeleRunner runner, RunnerConnection connection) { + super(runner, connection); + } - @Override - public boolean isResting() { - return true; - } + @Override + public boolean isResting() { + return true; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TeleRunnerState.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TeleRunnerState.java index 7c4490511..85d33da20 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TeleRunnerState.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TeleRunnerState.java @@ -10,58 +10,58 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A state in the tele-runner state machine. - */ +/** A state in the tele-runner state machine. */ public class TeleRunnerState implements State { - private static final Logger LOGGER = LoggerFactory.getLogger(TeleRunnerState.class); + private static final Logger LOGGER = LoggerFactory.getLogger(TeleRunnerState.class); - protected final TeleRunner runner; - protected final RunnerConnection connection; + protected final TeleRunner runner; + protected final RunnerConnection connection; - public TeleRunnerState(TeleRunner runner, RunnerConnection connection) { - this.runner = runner; - this.connection = connection; - } + public TeleRunnerState(TeleRunner runner, RunnerConnection connection) { + this.runner = runner; + this.connection = connection; + } - /** - * Called when a complete text packet has been received via the websocket connection. - * - * @param text The contents of the received packet - * @return the state to switch to next - */ - public TeleRunnerState onText(String text) { - Optional newState = connection.getSerializer() - .deserialize(text, ServerBoundPacket.class) - .flatMap(this::onPacket); + /** + * Called when a complete text packet has been received via the websocket connection. + * + * @param text The contents of the received packet + * @return the state to switch to next + */ + public TeleRunnerState onText(String text) { + Optional newState = + connection + .getSerializer() + .deserialize(text, ServerBoundPacket.class) + .flatMap(this::onPacket); - // If a packet has been received that could not be deserialized or handled, that is invalid - // behaviour. - if (newState.isEmpty()) { - LOGGER.info( - "Runner send a package I couldn't handle in {}. Content: '{}'", - getClass().getSimpleName(), text - ); - connection.close(StatusCode.ILLEGAL_PACKET); - } + // If a packet has been received that could not be deserialized or handled, that is invalid + // behaviour. + if (newState.isEmpty()) { + LOGGER.info( + "Runner send a package I couldn't handle in {}. Content: '{}'", + getClass().getSimpleName(), + text); + connection.close(StatusCode.ILLEGAL_PACKET); + } - return newState.orElse(this); - } + return newState.orElse(this); + } - /** - * Called by the default {@link #onText(String)} implementation if the text could be decoded to a - * {@link ServerBoundPacket}. - * - * @param packet the packet the text was deserialized into - * @return whether this function call handled the packet - */ - protected Optional onPacket(ServerBoundPacket packet) { - if (packet.getType() == ServerBoundPacketType.REQUEST_RUN) { - runner.prepareAndSendWork(); - return Optional.of(this); - } + /** + * Called by the default {@link #onText(String)} implementation if the text could be decoded to a + * {@link ServerBoundPacket}. + * + * @param packet the packet the text was deserialized into + * @return whether this function call handled the packet + */ + protected Optional onPacket(ServerBoundPacket packet) { + if (packet.getType() == ServerBoundPacketType.REQUEST_RUN) { + runner.prepareAndSendWork(); + return Optional.of(this); + } - return Optional.empty(); - } + return Optional.empty(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TimeoutState.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TimeoutState.java index c402db995..111016da5 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TimeoutState.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/runner/single/state/TimeoutState.java @@ -6,27 +6,25 @@ import de.aaaaaaah.velcom.shared.protocol.StatusCode; import de.aaaaaaah.velcom.shared.util.Timeout; -/** - * A state that waits for a given timeout before closing the connection. - */ +/** A state that waits for a given timeout before closing the connection. */ public abstract class TimeoutState extends TeleRunnerState { - private final Timeout timeout; + private final Timeout timeout; - public TimeoutState(TeleRunner runner, RunnerConnection connection) { - super(runner, connection); + public TimeoutState(TeleRunner runner, RunnerConnection connection) { + super(runner, connection); - timeout = Timeout.after(Delays.AWAIT_COMMAND_REPLY); - timeout.getCompletionStage().thenRun(() -> connection.close(StatusCode.COMMAND_TIMEOUT)); - } + timeout = Timeout.after(Delays.AWAIT_COMMAND_REPLY); + timeout.getCompletionStage().thenRun(() -> connection.close(StatusCode.COMMAND_TIMEOUT)); + } - @Override - public void onEnter() { - timeout.start(); - } + @Override + public void onEnter() { + timeout.start(); + } - @Override - public void onExit() { - timeout.cancel(); - } + @Override + public void onExit() { + timeout.cancel(); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/ManagedDirs.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/ManagedDirs.java index b6ec1d053..7c6340cab 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/ManagedDirs.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/ManagedDirs.java @@ -16,118 +16,107 @@ * directories, in addition to the config file. Files in these directories should never be modified * or deleted while VelCom is running. * - *

The {@code data/} directory contains the current state of VelCom. Files in this directory + *

The {@code data/} directory contains the current state of VelCom. Files in this directory * should never be deleted by an admin, only by VelCom itself. Only the config file and this * directory need to be included in backups. * - *

The {@code cache/} directory contains longer-lived information that can be deleted without + *

The {@code cache/} directory contains longer-lived information that can be deleted without * loss of data but may be costly to create again. * - *

The {@code tmp/} directory contains temporary files that VelCom deletes again when it - * doesn't need them any more. + *

The {@code tmp/} directory contains temporary files that VelCom deletes again when it doesn't + * need them any more. */ public class ManagedDirs { - private static final Logger LOGGER = LoggerFactory.getLogger(ManagedDirs.class); - - private final Path dataDir; - private final Path cacheDir; - private final Path tmpDir; - - public ManagedDirs(@Nullable Path dataDir, @Nullable Path cacheDir, @Nullable Path tmpDir) { - if (dataDir != null) { - this.dataDir = dataDir; - } else { - this.dataDir = Path.of("data/"); - } - - if (cacheDir != null) { - this.cacheDir = cacheDir; - } else { - this.cacheDir = Path.of("cache/"); - } - - if (tmpDir != null) { - this.tmpDir = tmpDir; - } else { - this.tmpDir = Path.of("tmp/"); - } - - LOGGER.info("Data dir: " + this.dataDir.toAbsolutePath()); - LOGGER.info("Cache dir: " + this.cacheDir.toAbsolutePath()); - LOGGER.info("Tmp dir: " + this.tmpDir.toAbsolutePath()); - } - - /** - * Create the directories as specified in the class docstring (if necessary) and make sure they - * only contain the allowed files and directories. Does not create any allowed files or - * directories that are absent. - * - * @throws IOException if the directories could not be properly created or cleaned - */ - public void createAndCleanDirs() throws IOException { - // Clean up data dir - onlyKeepAllowed( - dataDir, - Set.of("tars"), - Set.of("data.db", "data.db-shm", "data.db-wal") - ); - - // Clean up cache dir - onlyKeepAllowed( - cacheDir, - Set.of("repos"), - Set.of() - ); - - // Clean up tmp dir - onlyKeepAllowed( - tmpDir, - Set.of(), - Set.of() - ); - } - - private void onlyKeepAllowed(Path dir, Set allowedTopLevelDirs, - Set allowedTopLevelFiles) throws IOException { - - LOGGER.debug("Cleaning up {}", dir); - - Files.createDirectories(dir); - - // Using this collection and foreach instead of using the stream directly because of the - // possible IOExceptions inside the for loop. - List children = Files.list(dir).collect(Collectors.toList()); - for (Path child : children) { - String name = child.getFileName().toString(); - - if (Files.isDirectory(child)) { - if (!allowedTopLevelDirs.contains(name)) { - LOGGER.info("Removing dir {}", child); - FileHelper.deleteDirectoryOrFile(child); - } - } else { - if (!allowedTopLevelFiles.contains(name)) { - LOGGER.info("Removing file {}", child); - FileHelper.deleteDirectoryOrFile(child); - } - } - } - } - - public String getJdbcUrl() { - return "jdbc:sqlite:file:" + dataDir.resolve("data.db").toAbsolutePath(); - } - - public Path getTarsDir() { - return dataDir.resolve("tars/"); - } - - public Path getReposDir() { - return cacheDir.resolve("repos/"); - } - - public Path getArchivesDir() { - return tmpDir.resolve("archives/"); - } + private static final Logger LOGGER = LoggerFactory.getLogger(ManagedDirs.class); + + private final Path dataDir; + private final Path cacheDir; + private final Path tmpDir; + + public ManagedDirs(@Nullable Path dataDir, @Nullable Path cacheDir, @Nullable Path tmpDir) { + if (dataDir != null) { + this.dataDir = dataDir; + } else { + this.dataDir = Path.of("data/"); + } + + if (cacheDir != null) { + this.cacheDir = cacheDir; + } else { + this.cacheDir = Path.of("cache/"); + } + + if (tmpDir != null) { + this.tmpDir = tmpDir; + } else { + this.tmpDir = Path.of("tmp/"); + } + + LOGGER.info("Data dir: " + this.dataDir.toAbsolutePath()); + LOGGER.info("Cache dir: " + this.cacheDir.toAbsolutePath()); + LOGGER.info("Tmp dir: " + this.tmpDir.toAbsolutePath()); + } + + /** + * Create the directories as specified in the class docstring (if necessary) and make sure they + * only contain the allowed files and directories. Does not create any allowed files or + * directories that are absent. + * + * @throws IOException if the directories could not be properly created or cleaned + */ + public void createAndCleanDirs() throws IOException { + // Clean up data dir + onlyKeepAllowed(dataDir, Set.of("tars"), Set.of("data.db", "data.db-shm", "data.db-wal")); + + // Clean up cache dir + onlyKeepAllowed(cacheDir, Set.of("repos"), Set.of()); + + // Clean up tmp dir + onlyKeepAllowed(tmpDir, Set.of(), Set.of()); + } + + private void onlyKeepAllowed( + Path dir, Set allowedTopLevelDirs, Set allowedTopLevelFiles) + throws IOException { + + LOGGER.debug("Cleaning up {}", dir); + + Files.createDirectories(dir); + + // Using this collection and foreach instead of using the stream directly because of the + // possible IOExceptions inside the for loop. + List children = Files.list(dir).collect(Collectors.toList()); + for (Path child : children) { + String name = child.getFileName().toString(); + + if (Files.isDirectory(child)) { + if (!allowedTopLevelDirs.contains(name)) { + LOGGER.info("Removing dir {}", child); + FileHelper.deleteDirectoryOrFile(child); + } + } else { + if (!allowedTopLevelFiles.contains(name)) { + LOGGER.info("Removing file {}", child); + FileHelper.deleteDirectoryOrFile(child); + } + } + } + } + + public String getJdbcUrl() { + return "jdbc:sqlite:file:" + dataDir.resolve("data.db").toAbsolutePath(); + } + + public Path getTarsDir() { + return dataDir.resolve("tars/"); + } + + public Path getReposDir() { + return cacheDir.resolve("repos/"); + } + + public Path getArchivesDir() { + return tmpDir.resolve("archives/"); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBReadAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBReadAccess.java index 7c017ca45..48823baa8 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBReadAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBReadAccess.java @@ -4,24 +4,22 @@ import java.util.Objects; import org.jooq.DSLContext; -/** - * Allows read only access to a database. - */ +/** Allows read only access to a database. */ public class DBReadAccess implements Closeable { - protected final DSLContext ctx; + protected final DSLContext ctx; - public DBReadAccess(DSLContext ctx) { - this.ctx = Objects.requireNonNull(ctx); - } + public DBReadAccess(DSLContext ctx) { + this.ctx = Objects.requireNonNull(ctx); + } - @Override - public void close() { - // No longer needed - // TODO: Remove this and clear up all usages? - } + @Override + public void close() { + // No longer needed + // TODO: Remove this and clear up all usages? + } - public DSLContext dsl() { - return ctx; - } + public DSLContext dsl() { + return ctx; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBWriteAccess.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBWriteAccess.java index f912a2e44..e398f2731 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBWriteAccess.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DBWriteAccess.java @@ -6,33 +6,31 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Allows read and write access to a database. - */ +/** Allows read and write access to a database. */ public class DBWriteAccess extends DBReadAccess { - private static final Logger LOGGER = LoggerFactory.getLogger(DBWriteAccess.class); + private static final Logger LOGGER = LoggerFactory.getLogger(DBWriteAccess.class); - private final Lock lock; + private final Lock lock; - public DBWriteAccess(DSLContext ctx, Lock lock) { - super(ctx); - this.lock = Objects.requireNonNull(lock); - this.lock.lock(); - } + public DBWriteAccess(DSLContext ctx, Lock lock) { + super(ctx); + this.lock = Objects.requireNonNull(lock); + this.lock.lock(); + } - @Override - public void close() { - try { - super.close(); - } finally { - lock.unlock(); - } - } + @Override + public void close() { + try { + super.close(); + } finally { + lock.unlock(); + } + } - public void vacuum() { - LOGGER.info("Vacuuming database..."); - ctx.execute("VACUUM"); - LOGGER.info("Vacuuming completed."); - } + public void vacuum() { + LOGGER.info("Vacuuming database..."); + ctx.execute("VACUUM"); + LOGGER.info("Vacuuming completed."); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorage.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorage.java index 5b29ff68c..2fca5567f 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorage.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorage.java @@ -16,152 +16,153 @@ import org.sqlite.SQLiteConfig.JournalMode; import org.sqlite.SQLiteDataSource; -/** - * Provides access to a database. - */ +/** Provides access to a database. */ public class DatabaseStorage { - private final DSLContext context; - private final Lock writeLock = new ReentrantLock(); - - /** - * Initializes the database storage. - * - *

Also performs database migrations, if necessary. - * - * @param jdbcUrl the jdbc url used to connect to the database - */ - public DatabaseStorage(String jdbcUrl) { - migrate(jdbcUrl); - - SQLiteConfig sqliteConfig = new SQLiteConfig(); - sqliteConfig.enforceForeignKeys(true); - sqliteConfig.setJournalMode(JournalMode.WAL); - sqliteConfig.setJounalSizeLimit(1024 * 1024 * 8); // 8 MiB - - SQLiteDataSource sqLiteDataSource = new SQLiteDataSource(sqliteConfig); - sqLiteDataSource.setUrl(jdbcUrl); - - HikariConfig hikariConfig = new HikariConfig(); - hikariConfig.setDataSource(sqLiteDataSource); - hikariConfig.setPoolName("velcom-db-pool"); - hikariConfig.setMetricRegistry(Metrics.globalRegistry); - - HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig); - - context = DSL.using(hikariDataSource, SQLDialect.SQLITE); - } - - /** - * By default, sqlite doesn't check for foreign key constraints. By opening a new db connection - * only based on the db url, flyway can take advantage of this and migrations become much more - * performant. This does however mean that each migration has to check foreign key consistency - * itself using {@code PRAGMA foreign_key_check}. - * - * @param jdbcUrl the url to the database - */ - private void migrate(String jdbcUrl) { - Flyway flyway = Flyway.configure() - .dataSource(jdbcUrl, "", "") - .load(); - - flyway.migrate(); - } - - /** - * Acquires read access to the database. - * - * @return a way to interact with the database in a read only way - */ - public DBReadAccess acquireReadAccess() { - return new DBReadAccess(acquireContext()); - } - - /** - * Acquires read and write access to the database. - * - * @return a way to interact with the database - */ - public DBWriteAccess acquireWriteAccess() { - return new DBWriteAccess(acquireContext(), this.writeLock); - } - - /** - * Acquires a transaction that can only read from the database. - * - * @param handler the handler that is executed within the context of the transaction. The handler - * will be called once and its return value returned from the transaction. - * @param the handler's return type - * @return whatever the handler returned - */ - public T acquireReadTransaction(CheckedFunction handler) { - AtomicReference result = new AtomicReference<>(); - - try (final DBReadAccess db = acquireReadAccess()) { - db.dsl().transaction(cfg -> { - try (DBReadAccess inTransactionDB = new DBReadAccess(cfg.dsl())) { - result.set(handler.accept(inTransactionDB)); - } - }); - } - - return result.get(); - } - - /** - * Acquires a transaction that can only read from the database. - * - * @param handler the handler that is executed within the context of the transaction. It has no - * return value. - */ - public void acquireReadTransaction(CheckedConsumer handler) { - acquireWriteTransaction(db -> { - handler.accept(db); - return null; - }); - } - - /** - * Acquires a transaction that can read and write to the database. - * - * @param handler the handler that is executed within the context of the transaction. The handler - * will be called once and its return value returned from the transaction. - * @param the handler's return type - * @return whatever the handler returned - */ - public T acquireWriteTransaction(CheckedFunction handler) { - AtomicReference result = new AtomicReference<>(); - - try (final DBWriteAccess db = acquireWriteAccess()) { - db.dsl().transaction(cfg -> { - try (DBWriteAccess inTransactionDB = new DBWriteAccess(cfg.dsl(), this.writeLock)) { - result.set(handler.accept(inTransactionDB)); - } - }); - } - - return result.get(); - } - - /** - * Acquires a transaction that can read and write to the database. - * - * @param handler the handler that is executed within the context of the transaction. It has no - * return value. - */ - public void acquireWriteTransaction(CheckedConsumer handler) { - acquireWriteTransaction(db -> { - handler.accept(db); - return null; - }); - } - - /** - * @return a {@link DSLContext} instance providing jooq functionality along with a connection to - * the database - */ - private DSLContext acquireContext() { - return this.context; - } - + private final DSLContext context; + private final Lock writeLock = new ReentrantLock(); + + /** + * Initializes the database storage. + * + *

Also performs database migrations, if necessary. + * + * @param jdbcUrl the jdbc url used to connect to the database + */ + public DatabaseStorage(String jdbcUrl) { + migrate(jdbcUrl); + + SQLiteConfig sqliteConfig = new SQLiteConfig(); + sqliteConfig.enforceForeignKeys(true); + sqliteConfig.setJournalMode(JournalMode.WAL); + sqliteConfig.setJounalSizeLimit(1024 * 1024 * 8); // 8 MiB + + SQLiteDataSource sqLiteDataSource = new SQLiteDataSource(sqliteConfig); + sqLiteDataSource.setUrl(jdbcUrl); + + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setDataSource(sqLiteDataSource); + hikariConfig.setPoolName("velcom-db-pool"); + hikariConfig.setMetricRegistry(Metrics.globalRegistry); + + HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig); + + context = DSL.using(hikariDataSource, SQLDialect.SQLITE); + } + + /** + * By default, sqlite doesn't check for foreign key constraints. By opening a new db connection + * only based on the db url, flyway can take advantage of this and migrations become much more + * performant. This does however mean that each migration has to check foreign key consistency + * itself using {@code PRAGMA foreign_key_check}. + * + * @param jdbcUrl the url to the database + */ + private void migrate(String jdbcUrl) { + Flyway flyway = Flyway.configure().dataSource(jdbcUrl, "", "").load(); + + flyway.migrate(); + } + + /** + * Acquires read access to the database. + * + * @return a way to interact with the database in a read only way + */ + public DBReadAccess acquireReadAccess() { + return new DBReadAccess(acquireContext()); + } + + /** + * Acquires read and write access to the database. + * + * @return a way to interact with the database + */ + public DBWriteAccess acquireWriteAccess() { + return new DBWriteAccess(acquireContext(), this.writeLock); + } + + /** + * Acquires a transaction that can only read from the database. + * + * @param handler the handler that is executed within the context of the transaction. The handler + * will be called once and its return value returned from the transaction. + * @param the handler's return type + * @return whatever the handler returned + */ + public T acquireReadTransaction(CheckedFunction handler) { + AtomicReference result = new AtomicReference<>(); + + try (final DBReadAccess db = acquireReadAccess()) { + db.dsl() + .transaction( + cfg -> { + try (DBReadAccess inTransactionDB = new DBReadAccess(cfg.dsl())) { + result.set(handler.accept(inTransactionDB)); + } + }); + } + + return result.get(); + } + + /** + * Acquires a transaction that can only read from the database. + * + * @param handler the handler that is executed within the context of the transaction. It has no + * return value. + */ + public void acquireReadTransaction(CheckedConsumer handler) { + acquireWriteTransaction( + db -> { + handler.accept(db); + return null; + }); + } + + /** + * Acquires a transaction that can read and write to the database. + * + * @param handler the handler that is executed within the context of the transaction. The handler + * will be called once and its return value returned from the transaction. + * @param the handler's return type + * @return whatever the handler returned + */ + public T acquireWriteTransaction(CheckedFunction handler) { + AtomicReference result = new AtomicReference<>(); + + try (final DBWriteAccess db = acquireWriteAccess()) { + db.dsl() + .transaction( + cfg -> { + try (DBWriteAccess inTransactionDB = new DBWriteAccess(cfg.dsl(), this.writeLock)) { + result.set(handler.accept(inTransactionDB)); + } + }); + } + + return result.get(); + } + + /** + * Acquires a transaction that can read and write to the database. + * + * @param handler the handler that is executed within the context of the transaction. It has no + * return value. + */ + public void acquireWriteTransaction(CheckedConsumer handler) { + acquireWriteTransaction( + db -> { + handler.accept(db); + return null; + }); + } + + /** + * @return a {@link DSLContext} instance providing jooq functionality along with a connection to + * the database + */ + private DSLContext acquireContext() { + return this.context; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/GuickCloning.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/GuickCloning.java index db91cf1b3..c6fced181 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/GuickCloning.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/GuickCloning.java @@ -12,239 +12,231 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Allows for quicker git cloning if supported. - */ +/** Allows for quicker git cloning if supported. */ public abstract class GuickCloning { - private static final Logger LOGGER = LoggerFactory.getLogger(GuickCloning.class); - private static final String GIT_EXECUTABLE = "git"; - - private static final GuickCloning INSTANCE = findInstanceToUse(); - - private GuickCloning() { - } - - private static GuickCloning findInstanceToUse() { - try { - ProgramResult git = new ProgramExecutor() - .execute(GIT_EXECUTABLE, "--version") - .get(); - if (git.getExitCode() == 0) { - LOGGER.info("git executable found, using fast path for cloning"); - return new CmdGitCloning(); - } - } catch (InterruptedException | ExecutionException ignored) { - } - LOGGER.info("git executable not found, falling back to slow path for cloning"); - - return new JgitCloning(); - } - - /** - * Runs a usable instance of {@link GuickCloning}. - * - * @return the instance - */ - public static GuickCloning getInstance() { - return INSTANCE; - } - - /** - * Clones a given repo from source to target. - * - * @param source the source - * @param targetDir the target directory - * @throws CloneException if an error occurs - */ - public abstract void cloneMirror(String source, Path targetDir) throws CloneException; - - /** - * Clones a given repo and checks out the given commit. - * - * @param source the source to clone from - * @param targetDir the target directory - * @param commitHash the hash of the commit to check out - * @throws CloneException if an error occurs - */ - public abstract void cloneCommit(String source, Path targetDir, String commitHash) - throws CloneException; - - /** - * Updates a bare repo to the remote state. - * - * @param repoDir the path to the repo - * @throws CloneException if an error occurs - */ - public abstract void updateBareRepo(Path repoDir) throws CloneException; - - - /** - * An exception that occurred while cloning a repo. - */ - public static class CloneException extends Exception { - - public CloneException(String message) { - super(message); - } - - public CloneException(String message, Throwable cause) { - super(message, cause); - } - } - - /** - * Uses JGit for cloning. - */ - private static class JgitCloning extends GuickCloning { - - private JgitCloning() { - } - - @Override - public void cloneMirror(String source, Path targetDir) throws CloneException { - try { - CloneCommand cloneCommand = Git.cloneRepository() - .setURI(source) - .setDirectory(targetDir.toFile()) - .setBare(true); - cloneCommand.call().close(); - } catch (GitAPIException e) { - throw new CloneException("Could not clone bare repo", e); - } - } - - @Override - public void cloneCommit(String source, Path targetDir, String commitHash) - throws CloneException { - try (Git clone = Git.cloneRepository() - .setBare(false) - .setCloneSubmodules(true) - .setCloneAllBranches(false) - .setURI(source) - .setDirectory(targetDir.toFile()) - .call()) { - - clone.checkout().setName(commitHash).call(); - clone.submoduleInit().call(); - clone.submoduleSync().call(); - // Update submodules - clone.submoduleUpdate().setFetch(true).setStrategy(MergeStrategy.THEIRS).call(); - - // Use git clean to remove untracked submodules - clone.clean() - .setCleanDirectories(true) - .setForce(true) - .call(); - } catch (GitAPIException e) { - throw new CloneException("Error checking out commit", e); - } - } - - @Override - public void updateBareRepo(Path repoDir) throws CloneException { - try { - Git.open(repoDir.toFile()).fetch().setRemoveDeletedRefs(true).call(); - } catch (GitAPIException | IOException e) { - throw new CloneException( - "Error when pulling the latest changes from a bare git repo", - e - ); - } - } - } - - /** - * Uses the git executable for cloning. - */ - private static class CmdGitCloning extends GuickCloning { - - @Override - public void cloneMirror(String source, Path targetDir) throws CloneException { - try { - ProgramResult programResult = new ProgramExecutor() - .execute( - GIT_EXECUTABLE, "clone", "--mirror", - source, - targetDir.toAbsolutePath().toString() - ).get(); - guardResult(source, programResult); - } catch (InterruptedException | ExecutionException e) { - throw new CloneException("Clone failed for " + source + " (mirror)", e); - } - } - - private void guardResult(String message, ProgramResult programResult) - throws CloneException { - if (programResult.getExitCode() == 0) { - return; - } - throw new CloneException( - message - + "\nExit code: " + programResult.getExitCode() - + "\nStdout: " + programResult.getStdOut() - + "\nStderr: " + programResult.getStdErr() - + "\nAfter: " + programResult.getRuntime() - ); - } - - @Override - public void cloneCommit(String source, Path targetDir, String commitHash) - throws CloneException { - - try { - ProgramResult programResult = new ProgramExecutor() - .execute( - GIT_EXECUTABLE, "clone", "--recursive", "--recurse-submodules", - source, - targetDir.toAbsolutePath().toString() - ) - .get(); - guardResult( - "Clone failed", - programResult - ); - - programResult = new ProgramExecutor() - .execute( - GIT_EXECUTABLE, "-C", targetDir.toAbsolutePath().toString(), - "checkout", commitHash - ) - .get(); - - guardResult( - "Checkout failed!", - programResult - ); - - programResult = new ProgramExecutor() - .execute( - GIT_EXECUTABLE, "-C", targetDir.toAbsolutePath().toString(), - "submodule", "update", "--force", "--recursive" - ) - .get(); - - guardResult( - "Submodule checkout failed!", - programResult - ); - } catch (InterruptedException | ExecutionException e) { - throw new CloneException("Clone failed for " + source + " at " + commitHash, e); - } - } - - @Override - public void updateBareRepo(Path repoDir) throws CloneException { - try { - ProgramResult programResult = new ProgramExecutor() - .execute( - GIT_EXECUTABLE, "-C", repoDir.toAbsolutePath().toString(), - "fetch", "--all", "--prune" - ).get(); - guardResult("Fetch failed :/", programResult); - } catch (InterruptedException | ExecutionException e) { - throw new CloneException("Fetch failed for " + repoDir, e); - } - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(GuickCloning.class); + private static final String GIT_EXECUTABLE = "git"; + + private static final GuickCloning INSTANCE = findInstanceToUse(); + + private GuickCloning() {} + + private static GuickCloning findInstanceToUse() { + try { + ProgramResult git = new ProgramExecutor().execute(GIT_EXECUTABLE, "--version").get(); + if (git.getExitCode() == 0) { + LOGGER.info("git executable found, using fast path for cloning"); + return new CmdGitCloning(); + } + } catch (InterruptedException | ExecutionException ignored) { + } + LOGGER.info("git executable not found, falling back to slow path for cloning"); + + return new JgitCloning(); + } + + /** + * Runs a usable instance of {@link GuickCloning}. + * + * @return the instance + */ + public static GuickCloning getInstance() { + return INSTANCE; + } + + /** + * Clones a given repo from source to target. + * + * @param source the source + * @param targetDir the target directory + * @throws CloneException if an error occurs + */ + public abstract void cloneMirror(String source, Path targetDir) throws CloneException; + + /** + * Clones a given repo and checks out the given commit. + * + * @param source the source to clone from + * @param targetDir the target directory + * @param commitHash the hash of the commit to check out + * @throws CloneException if an error occurs + */ + public abstract void cloneCommit(String source, Path targetDir, String commitHash) + throws CloneException; + + /** + * Updates a bare repo to the remote state. + * + * @param repoDir the path to the repo + * @throws CloneException if an error occurs + */ + public abstract void updateBareRepo(Path repoDir) throws CloneException; + + /** An exception that occurred while cloning a repo. */ + public static class CloneException extends Exception { + + public CloneException(String message) { + super(message); + } + + public CloneException(String message, Throwable cause) { + super(message, cause); + } + } + + /** Uses JGit for cloning. */ + private static class JgitCloning extends GuickCloning { + + private JgitCloning() {} + + @Override + public void cloneMirror(String source, Path targetDir) throws CloneException { + try { + CloneCommand cloneCommand = + Git.cloneRepository().setURI(source).setDirectory(targetDir.toFile()).setBare(true); + cloneCommand.call().close(); + } catch (GitAPIException e) { + throw new CloneException("Could not clone bare repo", e); + } + } + + @Override + public void cloneCommit(String source, Path targetDir, String commitHash) + throws CloneException { + try (Git clone = + Git.cloneRepository() + .setBare(false) + .setCloneSubmodules(true) + .setCloneAllBranches(false) + .setURI(source) + .setDirectory(targetDir.toFile()) + .call()) { + + clone.checkout().setName(commitHash).call(); + clone.submoduleInit().call(); + clone.submoduleSync().call(); + // Update submodules + clone.submoduleUpdate().setFetch(true).setStrategy(MergeStrategy.THEIRS).call(); + + // Use git clean to remove untracked submodules + clone.clean().setCleanDirectories(true).setForce(true).call(); + } catch (GitAPIException e) { + throw new CloneException("Error checking out commit", e); + } + } + + @Override + public void updateBareRepo(Path repoDir) throws CloneException { + try { + Git.open(repoDir.toFile()).fetch().setRemoveDeletedRefs(true).call(); + } catch (GitAPIException | IOException e) { + throw new CloneException("Error when pulling the latest changes from a bare git repo", e); + } + } + } + + /** Uses the git executable for cloning. */ + private static class CmdGitCloning extends GuickCloning { + + @Override + public void cloneMirror(String source, Path targetDir) throws CloneException { + try { + ProgramResult programResult = + new ProgramExecutor() + .execute( + GIT_EXECUTABLE, + "clone", + "--mirror", + source, + targetDir.toAbsolutePath().toString()) + .get(); + guardResult(source, programResult); + } catch (InterruptedException | ExecutionException e) { + throw new CloneException("Clone failed for " + source + " (mirror)", e); + } + } + + private void guardResult(String message, ProgramResult programResult) throws CloneException { + if (programResult.getExitCode() == 0) { + return; + } + throw new CloneException( + message + + "\nExit code: " + + programResult.getExitCode() + + "\nStdout: " + + programResult.getStdOut() + + "\nStderr: " + + programResult.getStdErr() + + "\nAfter: " + + programResult.getRuntime()); + } + + @Override + public void cloneCommit(String source, Path targetDir, String commitHash) + throws CloneException { + + try { + ProgramResult programResult = + new ProgramExecutor() + .execute( + GIT_EXECUTABLE, + "clone", + "--recursive", + "--recurse-submodules", + source, + targetDir.toAbsolutePath().toString()) + .get(); + guardResult("Clone failed", programResult); + + programResult = + new ProgramExecutor() + .execute( + GIT_EXECUTABLE, + "-C", + targetDir.toAbsolutePath().toString(), + "checkout", + commitHash) + .get(); + + guardResult("Checkout failed!", programResult); + + programResult = + new ProgramExecutor() + .execute( + GIT_EXECUTABLE, + "-C", + targetDir.toAbsolutePath().toString(), + "submodule", + "update", + "--force", + "--recursive") + .get(); + + guardResult("Submodule checkout failed!", programResult); + } catch (InterruptedException | ExecutionException e) { + throw new CloneException("Clone failed for " + source + " at " + commitHash, e); + } + } + + @Override + public void updateBareRepo(Path repoDir) throws CloneException { + try { + ProgramResult programResult = + new ProgramExecutor() + .execute( + GIT_EXECUTABLE, + "-C", + repoDir.toAbsolutePath().toString(), + "fetch", + "--all", + "--prune") + .get(); + guardResult("Fetch failed :/", programResult); + } catch (InterruptedException | ExecutionException e) { + throw new CloneException("Fetch failed for " + repoDir, e); + } + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorage.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorage.java index 3097efe74..f880e54d6 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorage.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorage.java @@ -25,199 +25,191 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A repo storage can clone and store git repositories and allows inspecting them via jgit. - */ +/** A repo storage can clone and store git repositories and allows inspecting them via jgit. */ public class RepoStorage { - private static final Logger LOGGER = LoggerFactory.getLogger(RepoStorage.class); - - private final Path rootDir; - private final ReadWriteLock lock = new ReentrantReadWriteLock(); - - /** - * Initialize a new repo storage. - * - * @param rootDir the directory where all repositories will be stored in - * @throws IOException if the directory could not be created - */ - public RepoStorage(Path rootDir) throws IOException { - this.rootDir = rootDir; - Files.createDirectories(rootDir); - - SshdSessionFactory factory = new KnownHostsIgnoringSshdFactory( - new JGitKeyCache(), - new DefaultProxyDataFactory() - ); - - SshSessionFactory.setInstance(factory); - } - - /** - * Checks whether or not this storage contains a repository located under the given directory - * name. - * - * @param dirName the directory name - * @return true, if there is a repository currently stored under the given directory name - */ - public boolean containsRepository(String dirName) { - Path path = rootDir.resolve(dirName); - return Files.exists(path) && Files.isDirectory(path); - } - - /** - * Find all locally stored repositories. - * - * @return a collection of paths pointing to all locally stored repositories - * @throws IOException if an I/O error occurs when trying to read the directories - */ - public List getRepoDirectories() throws IOException { - this.lock.readLock().lock(); - - try (Stream fileStream = Files.list(rootDir)) { - return fileStream - .filter(Files::isDirectory) - .collect(Collectors.toUnmodifiableList()); - } finally { - this.lock.readLock().unlock(); - } - } - - /** - * Adds a new repository to this storage by cloning it from the specified remote url and storing - * it to the directory with the specified name relative to the default root directory of this - * storage. - * - * @param dirName A unique directory name for this repository - * @param remoteUrl The remote url to clone the repository from - * @return the path of the directory containing the new repository - * @throws AddRepositoryException if an exception occurs while trying to add the repository - */ - public Path addRepository(String dirName, String remoteUrl) throws AddRepositoryException { - Path repoDir = rootDir.resolve(dirName); - - this.lock.writeLock().lock(); - try { - if (Files.exists(repoDir)) { - throw new DirectoryAlreadyExistsException(repoDir); - } - - Files.createDirectory(repoDir); - - long start = System.currentTimeMillis(); - LOGGER.info("Cloning repo from {} into {}", remoteUrl, dirName); - - GuickCloning.getInstance().cloneMirror(remoteUrl, repoDir); - - LOGGER.info("Cloning took {} ms", System.currentTimeMillis() - start); - - return repoDir; - } catch (DirectoryAlreadyExistsException e) { - throw new AddRepositoryException(dirName, remoteUrl, e); - } catch (Exception e) { - // try to clean up directory - try { - FileHelper.deleteDirectoryOrFile(repoDir); - } catch (Exception ignore) { - } - - throw new AddRepositoryException(dirName, remoteUrl, e); - } finally { - this.lock.writeLock().unlock(); - } - } - - /** - * Removes a repository from this storage by deleting its directory. - * - * @param dirName the name of the repository - * @throws IOException if an I/O exception occurs trying to delete the directory of the - * repository - */ - public void deleteRepository(String dirName) throws IOException { - this.lock.writeLock().lock(); - - try { - FileHelper.deleteDirectoryOrFile(rootDir.resolve(dirName)); - } finally { - this.lock.writeLock().unlock(); - } - } - - /** - * Acquires the repository located under the specified directory by acquiring the repo storage - * lock and executing the given handler. - * - * @param dirName the name of the directory - * @param handler the handler to execute - * @throws NoSuchRepositoryException if no repository is found under the specified directory - * @throws RepositoryAcquisitionException if the handler throws any exception - */ - public void acquireRepository(String dirName, CheckedConsumer handler) - throws NoSuchRepositoryException, RepositoryAcquisitionException { - - this.lock.readLock().lock(); - - try { - Path repoDir = getRepoDir(dirName); - - try (Git git = Git.open(repoDir.toFile())) { - // Since the Git object was created via a static factory method and had to create its own - // Repository, it will also close that Repository itself, meaning we don't have to close it - // explicitly. - Repository repository = git.getRepository(); - handler.accept(repository); - } catch (Exception e) { - throw new RepositoryAcquisitionException(this, dirName, e); - } - } finally { - this.lock.readLock().unlock(); - } - } - - /** - * Acquires the repository located under the specified directory by acquiring the repo storage - * lock and returning the repository. - * - *

Note that, in order to release the repo storage lock, the returned repository instance - * must be closed.

- * - * @param dirName the name of the directory - * @return Returns the jgit repository instance for this repository - * @throws RepositoryAcquisitionException if an exception occurs while trying to acquire the - * repository - * @throws NoSuchRepositoryException if no repository under the given directory exists - */ - public Repository acquireRepository(String dirName) - throws RepositoryAcquisitionException, NoSuchRepositoryException { - Path repoDir = getRepoDir(dirName); - - RepositoryBuilder b = new RepositoryBuilder(); - b.setGitDir(repoDir.toFile()); - - try { - // constructor acquires read lock for this storage - return new RepositoryLock(this.lock.readLock(), b); - } catch (IOException e) { - throw new RepositoryAcquisitionException(this, dirName, e); - } - } - - /** - * Gets the path to the directory whose name is the one specified. - * - * @param dirName the name of the directory where the repository is located at - * @return the path of a repository directory by the given directory name - * @throws NoSuchRepositoryException if no repository can be found at the specified directory - */ - public Path getRepoDir(String dirName) throws NoSuchRepositoryException { - Path repoDir = rootDir.resolve(dirName); - - if (Files.exists(repoDir) && Files.isDirectory(repoDir)) { - return rootDir.resolve(dirName); - } else { - throw new NoSuchRepositoryException(dirName, this); - } - } - + private static final Logger LOGGER = LoggerFactory.getLogger(RepoStorage.class); + + private final Path rootDir; + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + /** + * Initialize a new repo storage. + * + * @param rootDir the directory where all repositories will be stored in + * @throws IOException if the directory could not be created + */ + public RepoStorage(Path rootDir) throws IOException { + this.rootDir = rootDir; + Files.createDirectories(rootDir); + + SshdSessionFactory factory = + new KnownHostsIgnoringSshdFactory(new JGitKeyCache(), new DefaultProxyDataFactory()); + + SshSessionFactory.setInstance(factory); + } + + /** + * Checks whether or not this storage contains a repository located under the given directory + * name. + * + * @param dirName the directory name + * @return true, if there is a repository currently stored under the given directory name + */ + public boolean containsRepository(String dirName) { + Path path = rootDir.resolve(dirName); + return Files.exists(path) && Files.isDirectory(path); + } + + /** + * Find all locally stored repositories. + * + * @return a collection of paths pointing to all locally stored repositories + * @throws IOException if an I/O error occurs when trying to read the directories + */ + public List getRepoDirectories() throws IOException { + this.lock.readLock().lock(); + + try (Stream fileStream = Files.list(rootDir)) { + return fileStream.filter(Files::isDirectory).collect(Collectors.toUnmodifiableList()); + } finally { + this.lock.readLock().unlock(); + } + } + + /** + * Adds a new repository to this storage by cloning it from the specified remote url and storing + * it to the directory with the specified name relative to the default root directory of this + * storage. + * + * @param dirName A unique directory name for this repository + * @param remoteUrl The remote url to clone the repository from + * @return the path of the directory containing the new repository + * @throws AddRepositoryException if an exception occurs while trying to add the repository + */ + public Path addRepository(String dirName, String remoteUrl) throws AddRepositoryException { + Path repoDir = rootDir.resolve(dirName); + + this.lock.writeLock().lock(); + try { + if (Files.exists(repoDir)) { + throw new DirectoryAlreadyExistsException(repoDir); + } + + Files.createDirectory(repoDir); + + long start = System.currentTimeMillis(); + LOGGER.info("Cloning repo from {} into {}", remoteUrl, dirName); + + GuickCloning.getInstance().cloneMirror(remoteUrl, repoDir); + + LOGGER.info("Cloning took {} ms", System.currentTimeMillis() - start); + + return repoDir; + } catch (DirectoryAlreadyExistsException e) { + throw new AddRepositoryException(dirName, remoteUrl, e); + } catch (Exception e) { + // try to clean up directory + try { + FileHelper.deleteDirectoryOrFile(repoDir); + } catch (Exception ignore) { + } + + throw new AddRepositoryException(dirName, remoteUrl, e); + } finally { + this.lock.writeLock().unlock(); + } + } + + /** + * Removes a repository from this storage by deleting its directory. + * + * @param dirName the name of the repository + * @throws IOException if an I/O exception occurs trying to delete the directory of the repository + */ + public void deleteRepository(String dirName) throws IOException { + this.lock.writeLock().lock(); + + try { + FileHelper.deleteDirectoryOrFile(rootDir.resolve(dirName)); + } finally { + this.lock.writeLock().unlock(); + } + } + + /** + * Acquires the repository located under the specified directory by acquiring the repo storage + * lock and executing the given handler. + * + * @param dirName the name of the directory + * @param handler the handler to execute + * @throws NoSuchRepositoryException if no repository is found under the specified directory + * @throws RepositoryAcquisitionException if the handler throws any exception + */ + public void acquireRepository(String dirName, CheckedConsumer handler) + throws NoSuchRepositoryException, RepositoryAcquisitionException { + + this.lock.readLock().lock(); + + try { + Path repoDir = getRepoDir(dirName); + + try (Git git = Git.open(repoDir.toFile())) { + // Since the Git object was created via a static factory method and had to create its own + // Repository, it will also close that Repository itself, meaning we don't have to close it + // explicitly. + Repository repository = git.getRepository(); + handler.accept(repository); + } catch (Exception e) { + throw new RepositoryAcquisitionException(this, dirName, e); + } + } finally { + this.lock.readLock().unlock(); + } + } + + /** + * Acquires the repository located under the specified directory by acquiring the repo storage + * lock and returning the repository. + * + *

Note that, in order to release the repo storage lock, the returned repository instance + * must be closed. + * + * @param dirName the name of the directory + * @return Returns the jgit repository instance for this repository + * @throws RepositoryAcquisitionException if an exception occurs while trying to acquire the + * repository + * @throws NoSuchRepositoryException if no repository under the given directory exists + */ + public Repository acquireRepository(String dirName) + throws RepositoryAcquisitionException, NoSuchRepositoryException { + Path repoDir = getRepoDir(dirName); + + RepositoryBuilder b = new RepositoryBuilder(); + b.setGitDir(repoDir.toFile()); + + try { + // constructor acquires read lock for this storage + return new RepositoryLock(this.lock.readLock(), b); + } catch (IOException e) { + throw new RepositoryAcquisitionException(this, dirName, e); + } + } + + /** + * Gets the path to the directory whose name is the one specified. + * + * @param dirName the name of the directory where the repository is located at + * @return the path of a repository directory by the given directory name + * @throws NoSuchRepositoryException if no repository can be found at the specified directory + */ + public Path getRepoDir(String dirName) throws NoSuchRepositoryException { + Path repoDir = rootDir.resolve(dirName); + + if (Files.exists(repoDir) && Files.isDirectory(repoDir)) { + return rootDir.resolve(dirName); + } else { + throw new NoSuchRepositoryException(dirName, this); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepositoryLock.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepositoryLock.java index 91c351f22..bdaad4710 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepositoryLock.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/RepositoryLock.java @@ -40,454 +40,449 @@ */ public class RepositoryLock extends Repository { - private final Repository repository; - private final Lock readLock; - - /** - * Constructs a new repository lock. - * - * @param readLock the read lock to immediately acquire - * @param options options used to create the internal jgit repository instance - * @throws IOException if the repository could not be accessed to configure the rest of the - * builder's parameters. - */ - RepositoryLock(Lock readLock, BaseRepositoryBuilder options) throws IOException { - super(options); - this.repository = options.build(); - this.readLock = readLock; - this.readLock.lock(); - } - - public static ListenerList getGlobalListenerList() { - return Repository.getGlobalListenerList(); - } - - @Override - @NonNull - public ListenerList getListenerList() { - return repository.getListenerList(); - } - - @Override - public void fireEvent(RepositoryEvent event) { - repository.fireEvent(event); - } - - @Override - public void create() throws IOException { - repository.create(); - } - - @Override - public void create(boolean b) throws IOException { - repository.create(b); - } - - @Override - public File getDirectory() { - return repository.getDirectory(); - } - - @Override - public String getIdentifier() { - return repository.getIdentifier(); - } - - @Override - @NonNull - public ObjectDatabase getObjectDatabase() { - return repository.getObjectDatabase(); - } - - @Override - @NonNull - public ObjectInserter newObjectInserter() { - return repository.newObjectInserter(); - } - - @Override - @NonNull - public ObjectReader newObjectReader() { - return repository.newObjectReader(); - } - - @Override - @NonNull - public RefDatabase getRefDatabase() { - return repository.getRefDatabase(); - } - - @Override - @NonNull - public StoredConfig getConfig() { - return repository.getConfig(); - } - - @Override - @NonNull - public AttributesNodeProvider createAttributesNodeProvider() { - return repository.createAttributesNodeProvider(); - } - - @Override - public FS getFS() { - return repository.getFS(); - } - - @SuppressWarnings("deprecation") - @Override - @Deprecated - public boolean hasObject(AnyObjectId objectId) { - return repository.hasObject(objectId); - } - - @Override - @NonNull - public ObjectLoader open(AnyObjectId objectId) throws IOException { - return repository.open(objectId); - } - - @Override - @NonNull - public ObjectLoader open(AnyObjectId objectId, - int typeHint) throws IOException { - return repository.open(objectId, typeHint); - } - - @Override - @NonNull - public RefUpdate updateRef(String ref) throws IOException { - return repository.updateRef(ref); - } - - @Override - @NonNull - public RefUpdate updateRef(String ref, boolean detach) throws IOException { - return repository.updateRef(ref, detach); - } - - @Override - @NonNull - public RefRename renameRef(String fromRef, String toRef) throws IOException { - return repository.renameRef(fromRef, toRef); - } - - @Override - @Nullable - public ObjectId resolve(String revstr) - throws RevisionSyntaxException, IOException { - return repository.resolve(revstr); - } - - @Override - @Nullable - public String simplify(String revstr) throws IOException { - return repository.simplify(revstr); - } - - @Override - public void incrementOpen() { - repository.incrementOpen(); - } - - @Override - public void close() { - readLock.unlock(); - repository.close(); - } - - @Override - @NonNull - public String toString() { - return repository.toString(); - } - - @Override - @Nullable - public String getFullBranch() throws IOException { - return repository.getFullBranch(); - } - - @Override - @Nullable - public String getBranch() throws IOException { - return repository.getBranch(); - } - - @Override - @NonNull - public Set getAdditionalHaves() throws IOException { - return repository.getAdditionalHaves(); - } - - @SuppressWarnings("deprecation") - @Override - @NonNull - @Deprecated - public Map getAllRefs() { - return repository.getAllRefs(); - } - - @SuppressWarnings("deprecation") - @Override - @NonNull - @Deprecated - public Map getTags() { - return repository.getTags(); - } - - @SuppressWarnings("deprecation") - @Override - @NonNull - @Deprecated - public Ref peel(Ref ref) { - return repository.peel(ref); - } - - @Override - @NonNull - public Map> getAllRefsByPeeledObjectId() throws IOException { - return repository.getAllRefsByPeeledObjectId(); - } - - @Override - @NonNull - public File getIndexFile() throws NoWorkTreeException { - return repository.getIndexFile(); - } - - @Override - public RevCommit parseCommit(AnyObjectId id) throws IOException { - return repository.parseCommit(id); - } - - @Override - @NonNull - public DirCache readDirCache() throws NoWorkTreeException, IOException { - return repository.readDirCache(); - } - - @Override - @NonNull - public DirCache lockDirCache() throws NoWorkTreeException, IOException { - return repository.lockDirCache(); - } - - @Override - @NonNull - public RepositoryState getRepositoryState() { - return repository.getRepositoryState(); - } - - /** - * Check validity of a ref name. It must not contain character that has a special meaning in a Git - * object reference expression. Some other dangerous characters are also excluded. - * - *

For portability reasons '\' is excluded

- * - * @param refName a {@link java.lang.String} object. - * @return true if refName is a valid ref name - */ - public static boolean isValidRefName(String refName) { - return Repository.isValidRefName(refName); - } - - /** - * Normalizes the passed branch name into a possible valid branch name. The validity of the - * returned name should be checked by a subsequent call to {@link #isValidRefName(String)}. - * - *

- * Future implementations of this method could be more restrictive or more lenient about the - * validity of specific characters in the returned name. - * - *

- * The current implementation returns the trimmed input string if this is already a valid branch - * name. Otherwise it returns a trimmed string with special characters not allowed by {@link - * #isValidRefName(String)} replaced by hyphens ('-') and blanks replaced by underscores ('_'). - * Leading and trailing slashes, dots, hyphens, and underscores are removed. - * - * @param name to normalize - * @return The normalized name or an empty String if it is {@code null} or empty. - * @see #isValidRefName(String) - * @since 4.7 - */ - public static String normalizeBranchName(String name) { - return Repository.normalizeBranchName(name); - } - - /** - * Strip work dir and return normalized repository path. - * - * @param workDir Work dir - * @param file File whose path shall be stripped of its workdir - * @return normalized repository relative path or the empty string if the file is not relative to - * the work directory. - */ - @NonNull - public static String stripWorkDir(File workDir, File file) { - return Repository.stripWorkDir(workDir, file); - } - - @Override - public boolean isBare() { - return repository.isBare(); - } - - @Override - @NonNull - public File getWorkTree() throws NoWorkTreeException { - return repository.getWorkTree(); - } - - @Override - public void scanForRepoChanges() throws IOException { - repository.scanForRepoChanges(); - } - - @Override - public void notifyIndexChanged(boolean b) { - repository.notifyIndexChanged(b); - } - - /** - * Get a shortened more user friendly ref name. - * - * @param refName a {@link java.lang.String} object. - * @return a more user friendly ref name - */ - @NonNull - public static String shortenRefName(String refName) { - return Repository.shortenRefName(refName); - } - - @Override - @Nullable - public String shortenRemoteBranchName(String refName) { - return repository.shortenRemoteBranchName(refName); - } - - @Override - @Nullable - public String getRemoteName(String refName) { - return repository.getRemoteName(refName); - } - - @Override - @Nullable - public String getGitwebDescription() throws IOException { - return repository.getGitwebDescription(); - } - - @Override - public void setGitwebDescription(String description) throws IOException { - repository.setGitwebDescription(description); - } - - @Override - @Nullable - public ReflogReader getReflogReader(String s) throws IOException { - return repository.getReflogReader(s); - } - - @Override - @Nullable - public String readMergeCommitMsg() throws IOException, NoWorkTreeException { - return repository.readMergeCommitMsg(); - } - - @Override - public void writeMergeCommitMsg(String msg) throws IOException { - repository.writeMergeCommitMsg(msg); - } - - @Override - @Nullable - public String readCommitEditMsg() throws IOException, NoWorkTreeException { - return repository.readCommitEditMsg(); - } - - @Override - public void writeCommitEditMsg(String msg) throws IOException { - repository.writeCommitEditMsg(msg); - } - - @Override - @Nullable - public List readMergeHeads() throws IOException, NoWorkTreeException { - return repository.readMergeHeads(); - } - - @Override - public void writeMergeHeads(List heads) throws IOException { - repository.writeMergeHeads(heads); - } - - @Override - @Nullable - public ObjectId readCherryPickHead() throws IOException, NoWorkTreeException { - return repository.readCherryPickHead(); - } - - @Override - @Nullable - public ObjectId readRevertHead() throws IOException, NoWorkTreeException { - return repository.readRevertHead(); - } - - @Override - public void writeCherryPickHead(ObjectId head) throws IOException { - repository.writeCherryPickHead(head); - } - - @Override - public void writeRevertHead(ObjectId head) throws IOException { - repository.writeRevertHead(head); - } - - @Override - public void writeOrigHead(ObjectId head) throws IOException { - repository.writeOrigHead(head); - } - - @Override - @Nullable - public ObjectId readOrigHead() throws IOException, NoWorkTreeException { - return repository.readOrigHead(); - } - - @Override - @Nullable - public String readSquashCommitMsg() throws IOException { - return repository.readSquashCommitMsg(); - } - - @Override - public void writeSquashCommitMsg(String msg) throws IOException { - repository.writeSquashCommitMsg(msg); - } - - @Override - @NonNull - public List readRebaseTodo(String path, - boolean includeComments) throws IOException { - return repository.readRebaseTodo(path, includeComments); - } - - @Override - public void writeRebaseTodoFile(String path, - List steps, boolean append) throws IOException { - repository.writeRebaseTodoFile(path, steps, append); - } - - @Override - @NonNull - public Set getRemoteNames() { - return repository.getRemoteNames(); - } - - @Override - public void autoGC(ProgressMonitor monitor) { - repository.autoGC(monitor); - } - + private final Repository repository; + private final Lock readLock; + + /** + * Constructs a new repository lock. + * + * @param readLock the read lock to immediately acquire + * @param options options used to create the internal jgit repository instance + * @throws IOException if the repository could not be accessed to configure the rest of the + * builder's parameters. + */ + RepositoryLock(Lock readLock, BaseRepositoryBuilder options) throws IOException { + super(options); + this.repository = options.build(); + this.readLock = readLock; + this.readLock.lock(); + } + + public static ListenerList getGlobalListenerList() { + return Repository.getGlobalListenerList(); + } + + @Override + @NonNull + public ListenerList getListenerList() { + return repository.getListenerList(); + } + + @Override + public void fireEvent(RepositoryEvent event) { + repository.fireEvent(event); + } + + @Override + public void create() throws IOException { + repository.create(); + } + + @Override + public void create(boolean b) throws IOException { + repository.create(b); + } + + @Override + public File getDirectory() { + return repository.getDirectory(); + } + + @Override + public String getIdentifier() { + return repository.getIdentifier(); + } + + @Override + @NonNull + public ObjectDatabase getObjectDatabase() { + return repository.getObjectDatabase(); + } + + @Override + @NonNull + public ObjectInserter newObjectInserter() { + return repository.newObjectInserter(); + } + + @Override + @NonNull + public ObjectReader newObjectReader() { + return repository.newObjectReader(); + } + + @Override + @NonNull + public RefDatabase getRefDatabase() { + return repository.getRefDatabase(); + } + + @Override + @NonNull + public StoredConfig getConfig() { + return repository.getConfig(); + } + + @Override + @NonNull + public AttributesNodeProvider createAttributesNodeProvider() { + return repository.createAttributesNodeProvider(); + } + + @Override + public FS getFS() { + return repository.getFS(); + } + + @SuppressWarnings("deprecation") + @Override + @Deprecated + public boolean hasObject(AnyObjectId objectId) { + return repository.hasObject(objectId); + } + + @Override + @NonNull + public ObjectLoader open(AnyObjectId objectId) throws IOException { + return repository.open(objectId); + } + + @Override + @NonNull + public ObjectLoader open(AnyObjectId objectId, int typeHint) throws IOException { + return repository.open(objectId, typeHint); + } + + @Override + @NonNull + public RefUpdate updateRef(String ref) throws IOException { + return repository.updateRef(ref); + } + + @Override + @NonNull + public RefUpdate updateRef(String ref, boolean detach) throws IOException { + return repository.updateRef(ref, detach); + } + + @Override + @NonNull + public RefRename renameRef(String fromRef, String toRef) throws IOException { + return repository.renameRef(fromRef, toRef); + } + + @Override + @Nullable + public ObjectId resolve(String revstr) throws RevisionSyntaxException, IOException { + return repository.resolve(revstr); + } + + @Override + @Nullable + public String simplify(String revstr) throws IOException { + return repository.simplify(revstr); + } + + @Override + public void incrementOpen() { + repository.incrementOpen(); + } + + @Override + public void close() { + readLock.unlock(); + repository.close(); + } + + @Override + @NonNull + public String toString() { + return repository.toString(); + } + + @Override + @Nullable + public String getFullBranch() throws IOException { + return repository.getFullBranch(); + } + + @Override + @Nullable + public String getBranch() throws IOException { + return repository.getBranch(); + } + + @Override + @NonNull + public Set getAdditionalHaves() throws IOException { + return repository.getAdditionalHaves(); + } + + @SuppressWarnings("deprecation") + @Override + @NonNull + @Deprecated + public Map getAllRefs() { + return repository.getAllRefs(); + } + + @SuppressWarnings("deprecation") + @Override + @NonNull + @Deprecated + public Map getTags() { + return repository.getTags(); + } + + @SuppressWarnings("deprecation") + @Override + @NonNull + @Deprecated + public Ref peel(Ref ref) { + return repository.peel(ref); + } + + @Override + @NonNull + public Map> getAllRefsByPeeledObjectId() throws IOException { + return repository.getAllRefsByPeeledObjectId(); + } + + @Override + @NonNull + public File getIndexFile() throws NoWorkTreeException { + return repository.getIndexFile(); + } + + @Override + public RevCommit parseCommit(AnyObjectId id) throws IOException { + return repository.parseCommit(id); + } + + @Override + @NonNull + public DirCache readDirCache() throws NoWorkTreeException, IOException { + return repository.readDirCache(); + } + + @Override + @NonNull + public DirCache lockDirCache() throws NoWorkTreeException, IOException { + return repository.lockDirCache(); + } + + @Override + @NonNull + public RepositoryState getRepositoryState() { + return repository.getRepositoryState(); + } + + /** + * Check validity of a ref name. It must not contain character that has a special meaning in a Git + * object reference expression. Some other dangerous characters are also excluded. + * + *

For portability reasons '\' is excluded + * + * @param refName a {@link java.lang.String} object. + * @return true if refName is a valid ref name + */ + public static boolean isValidRefName(String refName) { + return Repository.isValidRefName(refName); + } + + /** + * Normalizes the passed branch name into a possible valid branch name. The validity of the + * returned name should be checked by a subsequent call to {@link #isValidRefName(String)}. + * + *

Future implementations of this method could be more restrictive or more lenient about the + * validity of specific characters in the returned name. + * + *

The current implementation returns the trimmed input string if this is already a valid + * branch name. Otherwise it returns a trimmed string with special characters not allowed by + * {@link #isValidRefName(String)} replaced by hyphens ('-') and blanks replaced by underscores + * ('_'). Leading and trailing slashes, dots, hyphens, and underscores are removed. + * + * @param name to normalize + * @return The normalized name or an empty String if it is {@code null} or empty. + * @see #isValidRefName(String) + * @since 4.7 + */ + public static String normalizeBranchName(String name) { + return Repository.normalizeBranchName(name); + } + + /** + * Strip work dir and return normalized repository path. + * + * @param workDir Work dir + * @param file File whose path shall be stripped of its workdir + * @return normalized repository relative path or the empty string if the file is not relative to + * the work directory. + */ + @NonNull + public static String stripWorkDir(File workDir, File file) { + return Repository.stripWorkDir(workDir, file); + } + + @Override + public boolean isBare() { + return repository.isBare(); + } + + @Override + @NonNull + public File getWorkTree() throws NoWorkTreeException { + return repository.getWorkTree(); + } + + @Override + public void scanForRepoChanges() throws IOException { + repository.scanForRepoChanges(); + } + + @Override + public void notifyIndexChanged(boolean b) { + repository.notifyIndexChanged(b); + } + + /** + * Get a shortened more user friendly ref name. + * + * @param refName a {@link java.lang.String} object. + * @return a more user friendly ref name + */ + @NonNull + public static String shortenRefName(String refName) { + return Repository.shortenRefName(refName); + } + + @Override + @Nullable + public String shortenRemoteBranchName(String refName) { + return repository.shortenRemoteBranchName(refName); + } + + @Override + @Nullable + public String getRemoteName(String refName) { + return repository.getRemoteName(refName); + } + + @Override + @Nullable + public String getGitwebDescription() throws IOException { + return repository.getGitwebDescription(); + } + + @Override + public void setGitwebDescription(String description) throws IOException { + repository.setGitwebDescription(description); + } + + @Override + @Nullable + public ReflogReader getReflogReader(String s) throws IOException { + return repository.getReflogReader(s); + } + + @Override + @Nullable + public String readMergeCommitMsg() throws IOException, NoWorkTreeException { + return repository.readMergeCommitMsg(); + } + + @Override + public void writeMergeCommitMsg(String msg) throws IOException { + repository.writeMergeCommitMsg(msg); + } + + @Override + @Nullable + public String readCommitEditMsg() throws IOException, NoWorkTreeException { + return repository.readCommitEditMsg(); + } + + @Override + public void writeCommitEditMsg(String msg) throws IOException { + repository.writeCommitEditMsg(msg); + } + + @Override + @Nullable + public List readMergeHeads() throws IOException, NoWorkTreeException { + return repository.readMergeHeads(); + } + + @Override + public void writeMergeHeads(List heads) throws IOException { + repository.writeMergeHeads(heads); + } + + @Override + @Nullable + public ObjectId readCherryPickHead() throws IOException, NoWorkTreeException { + return repository.readCherryPickHead(); + } + + @Override + @Nullable + public ObjectId readRevertHead() throws IOException, NoWorkTreeException { + return repository.readRevertHead(); + } + + @Override + public void writeCherryPickHead(ObjectId head) throws IOException { + repository.writeCherryPickHead(head); + } + + @Override + public void writeRevertHead(ObjectId head) throws IOException { + repository.writeRevertHead(head); + } + + @Override + public void writeOrigHead(ObjectId head) throws IOException { + repository.writeOrigHead(head); + } + + @Override + @Nullable + public ObjectId readOrigHead() throws IOException, NoWorkTreeException { + return repository.readOrigHead(); + } + + @Override + @Nullable + public String readSquashCommitMsg() throws IOException { + return repository.readSquashCommitMsg(); + } + + @Override + public void writeSquashCommitMsg(String msg) throws IOException { + repository.writeSquashCommitMsg(msg); + } + + @Override + @NonNull + public List readRebaseTodo(String path, boolean includeComments) + throws IOException { + return repository.readRebaseTodo(path, includeComments); + } + + @Override + public void writeRebaseTodoFile(String path, List steps, boolean append) + throws IOException { + repository.writeRebaseTodoFile(path, steps, append); + } + + @Override + @NonNull + public Set getRemoteNames() { + return repository.getRemoteNames(); + } + + @Override + public void autoGC(ProgressMonitor monitor) { + repository.autoGC(monitor); + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/AddRepositoryException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/AddRepositoryException.java index be57acc44..66472abd9 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/AddRepositoryException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/AddRepositoryException.java @@ -2,39 +2,36 @@ import java.util.Objects; -/** - * An exception that occurs when trying to add a repository to a repo storage fails. - */ +/** An exception that occurs when trying to add a repository to a repo storage fails. */ public class AddRepositoryException extends Exception { - private final String dirName; - private final String remoteUrl; + private final String dirName; + private final String remoteUrl; - /** - * Constructs a new {@link AddRepositoryException}. - * - * @param dirName the directory name for the repository - * @param remoteUrl the remote url of the repository - * @param cause the cause - */ - public AddRepositoryException(String dirName, String remoteUrl, Throwable cause) { - super("failed to add repository to directory \"" + dirName + "\": " + remoteUrl, cause); - this.dirName = Objects.requireNonNull(dirName); - this.remoteUrl = Objects.requireNonNull(remoteUrl); - } + /** + * Constructs a new {@link AddRepositoryException}. + * + * @param dirName the directory name for the repository + * @param remoteUrl the remote url of the repository + * @param cause the cause + */ + public AddRepositoryException(String dirName, String remoteUrl, Throwable cause) { + super("failed to add repository to directory \"" + dirName + "\": " + remoteUrl, cause); + this.dirName = Objects.requireNonNull(dirName); + this.remoteUrl = Objects.requireNonNull(remoteUrl); + } - /** - * @return Returns the directory that should have been the directory for the repository - */ - public String getDirName() { - return dirName; - } - - /** - * @return Returns the remote url of the repository - */ - public String getRemoteUrl() { - return remoteUrl; - } + /** + * @return Returns the directory that should have been the directory for the repository + */ + public String getDirName() { + return dirName; + } + /** + * @return Returns the remote url of the repository + */ + public String getRemoteUrl() { + return remoteUrl; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/DirectoryAlreadyExistsException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/DirectoryAlreadyExistsException.java index b82c85c16..36dd2c4aa 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/DirectoryAlreadyExistsException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/DirectoryAlreadyExistsException.java @@ -3,27 +3,24 @@ import java.nio.file.Path; import java.util.Objects; -/** - * An exception that is thrown when a certain directory already exists. - */ +/** An exception that is thrown when a certain directory already exists. */ public class DirectoryAlreadyExistsException extends Exception { - private final Path directory; + private final Path directory; - /** - * Constructs a new {@link DirectoryAlreadyExistsException}. - * - * @param directory the directory that already exists - */ - public DirectoryAlreadyExistsException(Path directory) { - this.directory = Objects.requireNonNull(directory); - } - - /** - * @return Returns the directry that already exists - */ - public Path getDirectory() { - return directory; - } + /** + * Constructs a new {@link DirectoryAlreadyExistsException}. + * + * @param directory the directory that already exists + */ + public DirectoryAlreadyExistsException(Path directory) { + this.directory = Objects.requireNonNull(directory); + } + /** + * @return Returns the directry that already exists + */ + public Path getDirectory() { + return directory; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/NoSuchRepositoryException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/NoSuchRepositoryException.java index 6daebfa5c..3664d7373 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/NoSuchRepositoryException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/NoSuchRepositoryException.java @@ -9,33 +9,32 @@ */ public class NoSuchRepositoryException extends NoSuchElementException { - private final String dirName; - private final RepoStorage storage; + private final String dirName; + private final RepoStorage storage; - /** - * Consturcts a new {@link NoSuchRepositoryException}. - * - * @param dirName the directory that is missing a repository - * @param storage the storage - */ - public NoSuchRepositoryException(String dirName, RepoStorage storage) { - super("no repository exists with given name " + dirName + " in storage " + storage); - this.dirName = dirName; - this.storage = storage; - } + /** + * Consturcts a new {@link NoSuchRepositoryException}. + * + * @param dirName the directory that is missing a repository + * @param storage the storage + */ + public NoSuchRepositoryException(String dirName, RepoStorage storage) { + super("no repository exists with given name " + dirName + " in storage " + storage); + this.dirName = dirName; + this.storage = storage; + } - /** - * @return the directory that is missing the repository - */ - public String getDirName() { - return dirName; - } - - /** - * @return the storage where the repository is missing in - */ - public RepoStorage getStorage() { - return storage; - } + /** + * @return the directory that is missing the repository + */ + public String getDirName() { + return dirName; + } + /** + * @return the storage where the repository is missing in + */ + public RepoStorage getStorage() { + return storage; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/RepositoryAcquisitionException.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/RepositoryAcquisitionException.java index 850522dd6..ed68c1d16 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/RepositoryAcquisitionException.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/repo/exception/RepositoryAcquisitionException.java @@ -2,39 +2,36 @@ import de.aaaaaaah.velcom.backend.storage.repo.RepoStorage; -/** - * An exception that occurs when the acquisition of a local repository has failed. - */ +/** An exception that occurs when the acquisition of a local repository has failed. */ public class RepositoryAcquisitionException extends Exception { - private final RepoStorage repoStorage; - private final String dirName; + private final RepoStorage repoStorage; + private final String dirName; - /** - * Constructs a new {@link RepositoryAcquisitionException}. - * - * @param repoStorage the storage wherein the acquisition was attempted - * @param dirName the directory name of the repository to acquire - * @param e the cause for this exception - */ - public RepositoryAcquisitionException(RepoStorage repoStorage, String dirName, Exception e) { - super("Failed to acquire repository " + dirName + " in storage " + repoStorage, e); - this.repoStorage = repoStorage; - this.dirName = dirName; - } + /** + * Constructs a new {@link RepositoryAcquisitionException}. + * + * @param repoStorage the storage wherein the acquisition was attempted + * @param dirName the directory name of the repository to acquire + * @param e the cause for this exception + */ + public RepositoryAcquisitionException(RepoStorage repoStorage, String dirName, Exception e) { + super("Failed to acquire repository " + dirName + " in storage " + repoStorage, e); + this.repoStorage = repoStorage; + this.dirName = dirName; + } - /** - * @return the storage wherein the acquisition was attempted - */ - public RepoStorage getRepoStorage() { - return repoStorage; - } - - /** - * @return the directory name of the repository to acquire - */ - public String getDirName() { - return dirName; - } + /** + * @return the storage wherein the acquisition was attempted + */ + public RepoStorage getRepoStorage() { + return repoStorage; + } + /** + * @return the directory name of the repository to acquire + */ + public String getDirName() { + return dirName; + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/tar/TarFileStorage.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/tar/TarFileStorage.java index b36236d3e..f3495d472 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/tar/TarFileStorage.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/storage/tar/TarFileStorage.java @@ -12,113 +12,104 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A tar file storage can store tar files associated to an identifier (usually a task/run id). - */ +/** A tar file storage can store tar files associated to an identifier (usually a task/run id). */ public class TarFileStorage { - private final static Logger LOGGER = LoggerFactory.getLogger(TarFileStorage.class); - - private final Path rootDir; - - /** - * Initialize a new tar file storage. - * - * @param rootDir the directory where the tar files will be stored in - * @throws IOException if the directory could not be created - */ - public TarFileStorage(Path rootDir) throws IOException { - this.rootDir = rootDir; - Files.createDirectories(rootDir); - } - - private Path getPathOfIdentifier(String identifier) { - return rootDir.resolve(identifier + ".tar"); - } - - private String getIdentifierOfPath(Path path) { - String name = path.getFileName().toString(); - - if (name.endsWith(".tar")) { - return name.substring(0, name.length() - 4); - } else { - return name; - } - } - - /** - * Add a new tar file to the storage. - * - * @param identifier the identifier (must be a valid file name) - * @param inputStream the stream containing the tar file - * @throws IOException if such a tar file already exists for the identifier or some file - * operation went wrong - */ - public void storeTarFile(String identifier, InputStream inputStream) throws IOException { - Files.copy( - inputStream, - getPathOfIdentifier(identifier) - ); - } - - /** - * Remove a tar file from the storage if it exists. Does nothing if the file doesn't exist. - * - * @param identifier the identifier (must be a valid file name) - * @throws IOException if some file operation went wrong - */ - public void removeTarFile(String identifier) throws IOException { - FileHelper.deleteDirectoryOrFile(getPathOfIdentifier(identifier)); - } - - /** - * Retrieve a tar file from the storage. - * - * @param identifier the identifier (must be a valid file name) - * @param outputStream the stream the tar file is written to - * @throws IOException if no tar file exists for the identifier or some file operation went - * wrong - */ - public void retrieveTarFile(String identifier, OutputStream outputStream) throws IOException { - Files.copy( - getPathOfIdentifier(identifier), - outputStream - ); - } - - /** - * Remove all files with unknown identifiers. - * - * @param knownIdentifiers a set of all known identifiers - * @throws IOException if some file operation went wrong - */ - public void removeUnknownFiles(Set knownIdentifiers) throws IOException { - LOGGER.debug("Removing files with unknown identifiers"); - - List children = Files.list(rootDir).collect(Collectors.toList()); - for (Path child : children) { - String identifier = getIdentifierOfPath(child); - - if (!knownIdentifiers.contains(identifier)) { - LOGGER.info("Removing unknown file {}", child); - FileHelper.deleteDirectoryOrFile(child); - } - } - } - - /** - * Remove all currently stored tar files. - * - * @throws IOException if some file operation went wrong - */ - public void removeAllFiles() throws IOException { - LOGGER.debug("Removing all files"); - - List children = Files.list(rootDir).collect(Collectors.toList()); - for (Path child : children) { - String identifier = getIdentifierOfPath(child); - LOGGER.info("Removing file {}", child); - FileHelper.deleteDirectoryOrFile(child); - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(TarFileStorage.class); + + private final Path rootDir; + + /** + * Initialize a new tar file storage. + * + * @param rootDir the directory where the tar files will be stored in + * @throws IOException if the directory could not be created + */ + public TarFileStorage(Path rootDir) throws IOException { + this.rootDir = rootDir; + Files.createDirectories(rootDir); + } + + private Path getPathOfIdentifier(String identifier) { + return rootDir.resolve(identifier + ".tar"); + } + + private String getIdentifierOfPath(Path path) { + String name = path.getFileName().toString(); + + if (name.endsWith(".tar")) { + return name.substring(0, name.length() - 4); + } else { + return name; + } + } + + /** + * Add a new tar file to the storage. + * + * @param identifier the identifier (must be a valid file name) + * @param inputStream the stream containing the tar file + * @throws IOException if such a tar file already exists for the identifier or some file operation + * went wrong + */ + public void storeTarFile(String identifier, InputStream inputStream) throws IOException { + Files.copy(inputStream, getPathOfIdentifier(identifier)); + } + + /** + * Remove a tar file from the storage if it exists. Does nothing if the file doesn't exist. + * + * @param identifier the identifier (must be a valid file name) + * @throws IOException if some file operation went wrong + */ + public void removeTarFile(String identifier) throws IOException { + FileHelper.deleteDirectoryOrFile(getPathOfIdentifier(identifier)); + } + + /** + * Retrieve a tar file from the storage. + * + * @param identifier the identifier (must be a valid file name) + * @param outputStream the stream the tar file is written to + * @throws IOException if no tar file exists for the identifier or some file operation went wrong + */ + public void retrieveTarFile(String identifier, OutputStream outputStream) throws IOException { + Files.copy(getPathOfIdentifier(identifier), outputStream); + } + + /** + * Remove all files with unknown identifiers. + * + * @param knownIdentifiers a set of all known identifiers + * @throws IOException if some file operation went wrong + */ + public void removeUnknownFiles(Set knownIdentifiers) throws IOException { + LOGGER.debug("Removing files with unknown identifiers"); + + List children = Files.list(rootDir).collect(Collectors.toList()); + for (Path child : children) { + String identifier = getIdentifierOfPath(child); + + if (!knownIdentifiers.contains(identifier)) { + LOGGER.info("Removing unknown file {}", child); + FileHelper.deleteDirectoryOrFile(child); + } + } + } + + /** + * Remove all currently stored tar files. + * + * @throws IOException if some file operation went wrong + */ + public void removeAllFiles() throws IOException { + LOGGER.debug("Removing all files"); + + List children = Files.list(rootDir).collect(Collectors.toList()); + for (Path child : children) { + String identifier = getIdentifierOfPath(child); + LOGGER.info("Removing file {}", child); + FileHelper.deleteDirectoryOrFile(child); + } + } } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedConsumer.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedConsumer.java index 9e90ca232..cdc2a3ec3 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedConsumer.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedConsumer.java @@ -10,12 +10,11 @@ @FunctionalInterface public interface CheckedConsumer { - /** - * Performs this operation on the given argument. - * - * @param t the input argument - * @throws E an exception if any occurs during the execution of the operation - */ - void accept(T t) throws E; - + /** + * Performs this operation on the given argument. + * + * @param t the input argument + * @throws E an exception if any occurs during the execution of the operation + */ + void accept(T t) throws E; } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedFunction.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedFunction.java index bac9db649..6f7c9ca14 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedFunction.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/CheckedFunction.java @@ -11,13 +11,12 @@ @FunctionalInterface public interface CheckedFunction { - /** - * Performs this operation on the given argument. - * - * @param t the input argument - * @return R on successful evaulation - * @throws E an exception if any occurs during the execution of the operation - */ - R accept(T t) throws E; - + /** + * Performs this operation on the given argument. + * + * @param t the input argument + * @return R on successful evaulation + * @throws E an exception if any occurs during the execution of the operation + */ + R accept(T t) throws E; } diff --git a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/TransferUtils.java b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/TransferUtils.java index d3f695a41..aed08ffb4 100644 --- a/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/TransferUtils.java +++ b/backend/backend/src/main/java/de/aaaaaaah/velcom/backend/util/TransferUtils.java @@ -24,111 +24,107 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Some basic utils for transferring tars and repos to output streams. - */ +/** Some basic utils for transferring tars and repos to output streams. */ public class TransferUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(TransferUtils.class); - - private TransferUtils() { - throw new UnsupportedOperationException("no"); - } - - /** - * Creates a non bare clone of a repository that resides in the given repo storage and corresponds - * to the provided {@code dirName} by cloning the repo into the specified {@code destDir}. - * - * @param repoStorage the repo storage that contains the repository which should be cloned - * @param dirName the directory name of the repository - * @param destDir where the repository should be cloned to - * @param hash which state of the repository the clone should be at - * @throws RepositoryAcquisitionException if the repository could not be acquired - * @throws CloneException if the clone operation itself failed - * @throws IOException if some io error occurred - */ - public static void cloneRepo(RepoStorage repoStorage, String dirName, Path destDir, - CommitHash hash) - throws RepositoryAcquisitionException, CloneException, IOException { - - try (Repository repository = repoStorage.acquireRepository(dirName)) { - // Make sure that the commit actually exists before doing anything else - ObjectId commitId = repository.resolve(hash.getHash()); - Objects.requireNonNull(commitId, "unknown commit hash " - + hash + " for local repo " + dirName); - - // Check destinationCloneDir - if (Files.exists(destDir)) { - return; // clone already exists, no need to clone again - } - - // Clone does not yet exist => clone original local repo to cloneDir - Files.createDirectories(destDir); - - Path originalRepoPath = repoStorage.getRepoDir(dirName); - - GuickCloning.getInstance().cloneCommit( - originalRepoPath.toAbsolutePath().toString(), - destDir, - hash.getHash() - ); - } catch (Exception e) { - // clone operation failed => try to delete clone directory - try { - FileHelper.deleteDirectoryOrFile(destDir); - } catch (Exception ignore) { - } - - throw e; - } - } - - /** - * Creates a tar of the repository at the specified {@code repoDir} and simultaneously writes that - * tar into the given output stream. - * - *

Note that this method closes the provided output stream after it has finished. - * - * @param repoDir the path to the repository - * @param out where the tar should be written to - * @throws IOException if some io error occurred - */ - public static void tarRepo(Path repoDir, OutputStream out) throws IOException { - try (out) { - var tarOut = new TarArchiveOutputStream(out); - - tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); - tarOut.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); - - Files.walk(repoDir) - .filter(not(Files::isSymbolicLink)) - .forEach(handleError(file -> { - String relativePath = repoDir.relativize(file).toString(); - - TarArchiveEntry entry = new TarArchiveEntry(file.toFile(), relativePath); - entry.setMode(PermissionsHelper.toOctal(Files.getPosixFilePermissions(file))); - - tarOut.putArchiveEntry(entry); - - if (Files.isRegularFile(file)) { - try (InputStream in = Files.newInputStream(file)) { - IOUtils.copy(in, tarOut); - } - } - - tarOut.closeArchiveEntry(); - })); - } - } - - private static Consumer handleError(CheckedConsumer checkedConsumer) { - return path -> { - try { - checkedConsumer.accept(path); - } catch (Exception e) { - throw new RuntimeException(e); - } - }; - } - + private static final Logger LOGGER = LoggerFactory.getLogger(TransferUtils.class); + + private TransferUtils() { + throw new UnsupportedOperationException("no"); + } + + /** + * Creates a non bare clone of a repository that resides in the given repo storage and corresponds + * to the provided {@code dirName} by cloning the repo into the specified {@code destDir}. + * + * @param repoStorage the repo storage that contains the repository which should be cloned + * @param dirName the directory name of the repository + * @param destDir where the repository should be cloned to + * @param hash which state of the repository the clone should be at + * @throws RepositoryAcquisitionException if the repository could not be acquired + * @throws CloneException if the clone operation itself failed + * @throws IOException if some io error occurred + */ + public static void cloneRepo( + RepoStorage repoStorage, String dirName, Path destDir, CommitHash hash) + throws RepositoryAcquisitionException, CloneException, IOException { + + try (Repository repository = repoStorage.acquireRepository(dirName)) { + // Make sure that the commit actually exists before doing anything else + ObjectId commitId = repository.resolve(hash.getHash()); + Objects.requireNonNull( + commitId, "unknown commit hash " + hash + " for local repo " + dirName); + + // Check destinationCloneDir + if (Files.exists(destDir)) { + return; // clone already exists, no need to clone again + } + + // Clone does not yet exist => clone original local repo to cloneDir + Files.createDirectories(destDir); + + Path originalRepoPath = repoStorage.getRepoDir(dirName); + + GuickCloning.getInstance() + .cloneCommit(originalRepoPath.toAbsolutePath().toString(), destDir, hash.getHash()); + } catch (Exception e) { + // clone operation failed => try to delete clone directory + try { + FileHelper.deleteDirectoryOrFile(destDir); + } catch (Exception ignore) { + } + + throw e; + } + } + + /** + * Creates a tar of the repository at the specified {@code repoDir} and simultaneously writes that + * tar into the given output stream. + * + *

Note that this method closes the provided output stream after it has finished. + * + * @param repoDir the path to the repository + * @param out where the tar should be written to + * @throws IOException if some io error occurred + */ + public static void tarRepo(Path repoDir, OutputStream out) throws IOException { + try (out) { + var tarOut = new TarArchiveOutputStream(out); + + tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX); + tarOut.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX); + + Files.walk(repoDir) + .filter(not(Files::isSymbolicLink)) + .forEach( + handleError( + file -> { + String relativePath = repoDir.relativize(file).toString(); + + TarArchiveEntry entry = new TarArchiveEntry(file.toFile(), relativePath); + entry.setMode(PermissionsHelper.toOctal(Files.getPosixFilePermissions(file))); + + tarOut.putArchiveEntry(entry); + + if (Files.isRegularFile(file)) { + try (InputStream in = Files.newInputStream(file)) { + IOUtils.copy(in, tarOut); + } + } + + tarOut.closeArchiveEntry(); + })); + } + } + + private static Consumer handleError(CheckedConsumer checkedConsumer) { + return path -> { + try { + checkedConsumer.accept(path); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestCommit.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestCommit.java index 940b3dc2c..c32fce65b 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestCommit.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestCommit.java @@ -6,77 +6,87 @@ public class TestCommit { - private static String dummyContent() { - return String.valueOf(System.currentTimeMillis()); - } - - private final String message; - private final String file; - private final String content; - private final @Nullable - String branch; - private final @Nullable - Instant authorDate; - - public TestCommit(String message) { - this(message, "afile.txt", dummyContent(), null, null); - } - - public TestCommit(String message, Instant authorDate) { - this(message, "afile.txt", dummyContent(), null, authorDate); - } - - public TestCommit(String message, String branch) { - this(message, "afile.txt", dummyContent(), branch, null); - } - - public TestCommit(String message, String branch, Instant authorDate) { - this(message, "afile.txt", dummyContent(), branch, authorDate); - } - - public TestCommit(String message, String file, String content) { - this(message, file, content, null, null); - } - - public TestCommit(String message, String file, String content, - @Nullable String branch, @Nullable Instant authorDate) { - - this.message = message; - this.file = file; - this.content = content; - this.branch = branch; - this.authorDate = authorDate; - } - - public String getMessage() { - return message; - } - - public String getFile() { - return file; - } - - public String getContent() { - return content; - } - - public Optional getBranch() { - return Optional.ofNullable(branch); - } - - public Optional getAuthorDate() { - return Optional.ofNullable(authorDate); - } - - @Override - public String toString() { - return "TestCommit{" + - "message='" + message + '\'' + - ", file='" + file + '\'' + - ", content='" + content + '\'' + - ", branch='" + branch + '\'' + - ", authorDate=" + authorDate + - '}'; - } - + private static String dummyContent() { + return String.valueOf(System.currentTimeMillis()); + } + + private final String message; + private final String file; + private final String content; + private final @Nullable String branch; + private final @Nullable Instant authorDate; + + public TestCommit(String message) { + this(message, "afile.txt", dummyContent(), null, null); + } + + public TestCommit(String message, Instant authorDate) { + this(message, "afile.txt", dummyContent(), null, authorDate); + } + + public TestCommit(String message, String branch) { + this(message, "afile.txt", dummyContent(), branch, null); + } + + public TestCommit(String message, String branch, Instant authorDate) { + this(message, "afile.txt", dummyContent(), branch, authorDate); + } + + public TestCommit(String message, String file, String content) { + this(message, file, content, null, null); + } + + public TestCommit( + String message, + String file, + String content, + @Nullable String branch, + @Nullable Instant authorDate) { + + this.message = message; + this.file = file; + this.content = content; + this.branch = branch; + this.authorDate = authorDate; + } + + public String getMessage() { + return message; + } + + public String getFile() { + return file; + } + + public String getContent() { + return content; + } + + public Optional getBranch() { + return Optional.ofNullable(branch); + } + + public Optional getAuthorDate() { + return Optional.ofNullable(authorDate); + } + + @Override + public String toString() { + return "TestCommit{" + + "message='" + + message + + '\'' + + ", file='" + + file + + '\'' + + ", content='" + + content + + '\'' + + ", branch='" + + branch + + '\'' + + ", authorDate=" + + authorDate + + '}'; + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestDb.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestDb.java index 80c856853..4aaaee50e 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestDb.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestDb.java @@ -46,201 +46,244 @@ */ public class TestDb { - private static final String TEST_DB_NAME = "test.db"; - - private final String jdbcUrl; - private final DSLContext dslContext; - - /** - * Create a new sqlite db. This class can choose its own names for the db files, but they must be - * placed in the specified directory. The sqlite db is created with WAL. Foreign key checks are - * enabled. - * - * @param tmpDir the directory the db files are created in - */ - public TestDb(Path tmpDir) { - jdbcUrl = "jdbc:sqlite:file:" + (tmpDir.resolve(TEST_DB_NAME).toAbsolutePath()); - - // Migrate to get a useful schema - - Flyway flyway = Flyway.configure() - .dataSource(jdbcUrl, "", "") - .load(); - flyway.migrate(); - - // Connect to db with correct settings - - SQLiteConfig sqliteConfig = new SQLiteConfig(); - sqliteConfig.enforceForeignKeys(true); - sqliteConfig.setJournalMode(JournalMode.WAL); - - SQLiteDataSource sqLiteDataSource = new SQLiteDataSource(sqliteConfig); - sqLiteDataSource.setUrl(jdbcUrl); - - dslContext = DSL.using(sqLiteDataSource, SQLDialect.SQLITE); - } - - /** - * @return a {@link DSLContext} that can be used to interact with the database - */ - public DSLContext db() { - return dslContext; - } - - /** - * Close the database connection and return a jdbc url that can be used to connect to the file. - * WARNING: After this function is called, calling other functions on this object is forbidden. - * - * @return a jdbc url that can be used to connect to the database - */ - public String closeAndGetJdbcUrl() { - // TODO: Closing does not seem to be needed - return jdbcUrl; - } - - public void addRepo(RepoId repoId, String name, RemoteUrl remoteUrl) { - dslContext.batchInsert(new RepoRecord( - repoId.getIdAsString(), - name, - remoteUrl.getUrl(), - null, - null - )).execute(); - } - - public void addRepo(RepoId repoId) { - addRepo(repoId, "repo name", new RemoteUrl("https://foo.bar/baz.git")); - } - - public void addBranch(RepoId repoId, BranchName name, CommitHash commitHash, boolean tracked) { - dslContext.batchInsert(new BranchRecord( - repoId.getIdAsString(), - name.getName(), - commitHash.getHash(), - tracked - )).execute(); - } - - public void addCommit(RepoId repoId, CommitHash commitHash, boolean reachable, boolean tracked, - boolean everTracked, String author, Instant authorDate, String committer, Instant committerDate, - String message) { - - dslContext.batchInsert(new KnownCommitRecord( - repoId.getIdAsString(), - commitHash.getHash(), - reachable, - tracked, - everTracked, - author, - authorDate, - committer, - committerDate, - message - )).execute(); - } - - public void addCommit(RepoId repoId, CommitHash commitHash, boolean reachable, boolean tracked, - boolean everTracked, String message, Instant authorDate, Instant committerDate) { - - addCommit(repoId, commitHash, reachable, tracked, everTracked, "author", authorDate, - "committer", committerDate, message); - } - - public void addCommit(RepoId repoId, CommitHash commitHash) { - - addCommit(repoId, commitHash, true, true, true, "author", Instant.now(), "committer", - Instant.now(), "message"); - } - - public void addCommitRel(RepoId repoId, CommitHash parent, CommitHash child) { - dslContext.batchInsert(new CommitRelationshipRecord( - repoId.getIdAsString(), - parent.getHash(), - child.getHash() - )).execute(); - } - - public void addDimension(DimensionInfo dimensionInfo) { - dslContext.batchInsert(new DimensionRecord( - dimensionInfo.getDimension().getBenchmark(), - dimensionInfo.getDimension().getMetric(), - dimensionInfo.getUnit().getName(), - dimensionInfo.getInterpretation().getTextualRepresentation(), - dimensionInfo.isSignificant() - )).execute(); - } - - public void addDimension(Dimension dimension) { - addDimension(new DimensionInfo(dimension)); - } - - public void addRun(RunId runId, String author, String runnerName, String runnerInfo, - Instant startTime, Instant stopTime, Either source, - @Nullable RunError error) { - - dslContext.batchInsert(new RunRecord( - runId.getIdAsString(), - author, - runnerName, - runnerInfo, - startTime, - stopTime, - source.consume(cs -> Optional.of(cs.getRepoId()), TarSource::getRepoId) - .map(RepoId::getIdAsString) - .orElse(null), - source.getLeft() - .map(CommitSource::getHash) - .map(CommitHash::getHash) - .orElse(null), - source.getRight() - .map(TarSource::getDescription) - .orElse(null), - Optional.ofNullable(error) - .map(RunError::getType) - .map(RunErrorType::getTextualRepresentation) - .orElse(null), - Optional.ofNullable(error) - .map(RunError::getMessage) - .orElse(null) - )).execute(); - } - - public void addRun(RunId runId, Either source) { - addRun(runId, "author", "runnerName", "runnerInfo", Instant.now(), Instant.now(), source, null); - } - - public UUID addMeasurement(RunId runId, Dimension dimension, @Nullable Unit unit, - @Nullable Interpretation interpretation, Either result) { - - UUID measurementId = UUID.randomUUID(); - - dslContext.batchInsert(new MeasurementRecord( - measurementId.toString(), - runId.getIdAsString(), - dimension.getBenchmark(), - dimension.getMetric(), - Optional.ofNullable(unit) - .map(Unit::getName) - .orElse(null), - Optional.ofNullable(interpretation) - .map(Interpretation::getTextualRepresentation) - .orElse(null), - result.getLeft() - .map(MeasurementError::getErrorMessage) - .orElse(null) - )).execute(); - - result.getRight().ifPresent(values -> dslContext - .batchInsert(values.getValues().stream() - .map(value -> new MeasurementValueRecord(measurementId.toString(), value)) - .collect(toList())) - .execute()); - - return measurementId; - } - - public UUID addMeasurement(RunId runId, Dimension dimension) { - return addMeasurement(runId, dimension, Unit.DEFAULT, Interpretation.DEFAULT, - Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d)))); - } + private static final String TEST_DB_NAME = "test.db"; + + private final String jdbcUrl; + private final DSLContext dslContext; + + /** + * Create a new sqlite db. This class can choose its own names for the db files, but they must be + * placed in the specified directory. The sqlite db is created with WAL. Foreign key checks are + * enabled. + * + * @param tmpDir the directory the db files are created in + */ + public TestDb(Path tmpDir) { + jdbcUrl = "jdbc:sqlite:file:" + (tmpDir.resolve(TEST_DB_NAME).toAbsolutePath()); + + // Migrate to get a useful schema + + Flyway flyway = Flyway.configure().dataSource(jdbcUrl, "", "").load(); + flyway.migrate(); + + // Connect to db with correct settings + + SQLiteConfig sqliteConfig = new SQLiteConfig(); + sqliteConfig.enforceForeignKeys(true); + sqliteConfig.setJournalMode(JournalMode.WAL); + + SQLiteDataSource sqLiteDataSource = new SQLiteDataSource(sqliteConfig); + sqLiteDataSource.setUrl(jdbcUrl); + + dslContext = DSL.using(sqLiteDataSource, SQLDialect.SQLITE); + } + + /** + * @return a {@link DSLContext} that can be used to interact with the database + */ + public DSLContext db() { + return dslContext; + } + + /** + * Close the database connection and return a jdbc url that can be used to connect to the file. + * WARNING: After this function is called, calling other functions on this object is forbidden. + * + * @return a jdbc url that can be used to connect to the database + */ + public String closeAndGetJdbcUrl() { + // TODO: Closing does not seem to be needed + return jdbcUrl; + } + + public void addRepo(RepoId repoId, String name, RemoteUrl remoteUrl) { + dslContext + .batchInsert(new RepoRecord(repoId.getIdAsString(), name, remoteUrl.getUrl(), null, null)) + .execute(); + } + + public void addRepo(RepoId repoId) { + addRepo(repoId, "repo name", new RemoteUrl("https://foo.bar/baz.git")); + } + + public void addBranch(RepoId repoId, BranchName name, CommitHash commitHash, boolean tracked) { + dslContext + .batchInsert( + new BranchRecord(repoId.getIdAsString(), name.getName(), commitHash.getHash(), tracked)) + .execute(); + } + + public void addCommit( + RepoId repoId, + CommitHash commitHash, + boolean reachable, + boolean tracked, + boolean everTracked, + String author, + Instant authorDate, + String committer, + Instant committerDate, + String message) { + + dslContext + .batchInsert( + new KnownCommitRecord( + repoId.getIdAsString(), + commitHash.getHash(), + reachable, + tracked, + everTracked, + author, + authorDate, + committer, + committerDate, + message)) + .execute(); + } + + public void addCommit( + RepoId repoId, + CommitHash commitHash, + boolean reachable, + boolean tracked, + boolean everTracked, + String message, + Instant authorDate, + Instant committerDate) { + + addCommit( + repoId, + commitHash, + reachable, + tracked, + everTracked, + "author", + authorDate, + "committer", + committerDate, + message); + } + + public void addCommit(RepoId repoId, CommitHash commitHash) { + + addCommit( + repoId, + commitHash, + true, + true, + true, + "author", + Instant.now(), + "committer", + Instant.now(), + "message"); + } + + public void addCommitRel(RepoId repoId, CommitHash parent, CommitHash child) { + dslContext + .batchInsert( + new CommitRelationshipRecord(repoId.getIdAsString(), parent.getHash(), child.getHash())) + .execute(); + } + + public void addDimension(DimensionInfo dimensionInfo) { + dslContext + .batchInsert( + new DimensionRecord( + dimensionInfo.getDimension().getBenchmark(), + dimensionInfo.getDimension().getMetric(), + dimensionInfo.getUnit().getName(), + dimensionInfo.getInterpretation().getTextualRepresentation(), + dimensionInfo.isSignificant())) + .execute(); + } + + public void addDimension(Dimension dimension) { + addDimension(new DimensionInfo(dimension)); + } + + public void addRun( + RunId runId, + String author, + String runnerName, + String runnerInfo, + Instant startTime, + Instant stopTime, + Either source, + @Nullable RunError error) { + + dslContext + .batchInsert( + new RunRecord( + runId.getIdAsString(), + author, + runnerName, + runnerInfo, + startTime, + stopTime, + source + .consume(cs -> Optional.of(cs.getRepoId()), TarSource::getRepoId) + .map(RepoId::getIdAsString) + .orElse(null), + source.getLeft().map(CommitSource::getHash).map(CommitHash::getHash).orElse(null), + source.getRight().map(TarSource::getDescription).orElse(null), + Optional.ofNullable(error) + .map(RunError::getType) + .map(RunErrorType::getTextualRepresentation) + .orElse(null), + Optional.ofNullable(error).map(RunError::getMessage).orElse(null))) + .execute(); + } + + public void addRun(RunId runId, Either source) { + addRun(runId, "author", "runnerName", "runnerInfo", Instant.now(), Instant.now(), source, null); + } + + public UUID addMeasurement( + RunId runId, + Dimension dimension, + @Nullable Unit unit, + @Nullable Interpretation interpretation, + Either result) { + + UUID measurementId = UUID.randomUUID(); + + dslContext + .batchInsert( + new MeasurementRecord( + measurementId.toString(), + runId.getIdAsString(), + dimension.getBenchmark(), + dimension.getMetric(), + Optional.ofNullable(unit).map(Unit::getName).orElse(null), + Optional.ofNullable(interpretation) + .map(Interpretation::getTextualRepresentation) + .orElse(null), + result.getLeft().map(MeasurementError::getErrorMessage).orElse(null))) + .execute(); + + result + .getRight() + .ifPresent( + values -> + dslContext + .batchInsert( + values.getValues().stream() + .map( + value -> + new MeasurementValueRecord(measurementId.toString(), value)) + .collect(toList())) + .execute()); + + return measurementId; + } + + public UUID addMeasurement(RunId runId, Dimension dimension) { + return addMeasurement( + runId, + dimension, + Unit.DEFAULT, + Interpretation.DEFAULT, + Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d)))); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestRepo.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestRepo.java index 5a96b3f4c..b1737d3f5 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestRepo.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/TestRepo.java @@ -20,105 +20,103 @@ public class TestRepo { - private final Path repoDir; - private final Set branches = new HashSet<>(); - private final Map commitHashMap = new HashMap<>(); - private final Map revCommitMap = new HashMap<>(); - private final Map revByHashMap = new HashMap<>(); - private String currentBranch = "master"; - - public TestRepo(Path repoDir) throws GitAPIException, IOException { - Objects.requireNonNull(repoDir); - this.repoDir = repoDir; - this.branches.add("master"); - - if (!Files.exists(repoDir)) { - Files.createDirectory(repoDir); - } - Git.init().setBare(false).setDirectory(repoDir.toFile()).call(); - } - - public TestRepo(Path repoDir, TestCommit[] commits) throws GitAPIException, IOException { - this(repoDir); - for (TestCommit commit : commits) { - this.commit(commit); - } - } - - public TestRepo(Path repoDir, List commits) throws GitAPIException, IOException { - this(repoDir); - for (TestCommit commit : commits) { - this.commit(commit); - } - } - - public Optional getCommitHash(TestCommit testCommit) { - return Optional.ofNullable(commitHashMap.get(testCommit)); - } - - public Optional getRevCommit(TestCommit testCommit) { - return Optional.ofNullable(revCommitMap.get(testCommit)); - } - - public Optional getRevCommit(String commitHash) { - return Optional.ofNullable(revByHashMap.get(commitHash)); - } - - public void commit(TestCommit[] commits) throws IOException, GitAPIException { - for (TestCommit commit : commits) { - this.commit(commit); - } - } - - public RevCommit commit(TestCommit commit) - throws IOException, GitAPIException { - - final String branch = commit.getBranch().orElse("master"); - final PersonIdent author; - - if (commit.getAuthorDate().isPresent()) { - Date date = Date.from(commit.getAuthorDate().get()); - author = new PersonIdent("peter", "peter@email.com", date, TimeZone.getDefault()); - } else { - author = new PersonIdent("peter", "peter@gmail.com"); - } - - // Checkout correct branch - try (Git git = Git.open(repoDir.toFile())) { - if (!currentBranch.equals(branch)) { - if (branches.contains(branch)) { - // branch already exists - git.checkout().setName(branch).call(); - } else { - // new branch - git.checkout().setName(branch).setCreateBranch(true).call(); - branches.add(branch); - } - currentBranch = branch; - } - - // Write file and stage it - Files.write( - repoDir.resolve(commit.getFile()), - commit.getContent().getBytes(), - StandardOpenOption.CREATE - ); - - git.add().addFilepattern(commit.getFile()).call(); - - // Create the commit - RevCommit revCommit = git.commit() - .setAuthor(author) - .setCommitter("peter", "peter@email.com") - .setMessage(commit.getMessage()) - .call(); - - this.commitHashMap.put(commit, revCommit.getId().getName()); - this.revCommitMap.put(commit, revCommit); - this.revByHashMap.put(revCommit.getId().getName(), revCommit); - - return revCommit; - } - } - + private final Path repoDir; + private final Set branches = new HashSet<>(); + private final Map commitHashMap = new HashMap<>(); + private final Map revCommitMap = new HashMap<>(); + private final Map revByHashMap = new HashMap<>(); + private String currentBranch = "master"; + + public TestRepo(Path repoDir) throws GitAPIException, IOException { + Objects.requireNonNull(repoDir); + this.repoDir = repoDir; + this.branches.add("master"); + + if (!Files.exists(repoDir)) { + Files.createDirectory(repoDir); + } + Git.init().setBare(false).setDirectory(repoDir.toFile()).call(); + } + + public TestRepo(Path repoDir, TestCommit[] commits) throws GitAPIException, IOException { + this(repoDir); + for (TestCommit commit : commits) { + this.commit(commit); + } + } + + public TestRepo(Path repoDir, List commits) throws GitAPIException, IOException { + this(repoDir); + for (TestCommit commit : commits) { + this.commit(commit); + } + } + + public Optional getCommitHash(TestCommit testCommit) { + return Optional.ofNullable(commitHashMap.get(testCommit)); + } + + public Optional getRevCommit(TestCommit testCommit) { + return Optional.ofNullable(revCommitMap.get(testCommit)); + } + + public Optional getRevCommit(String commitHash) { + return Optional.ofNullable(revByHashMap.get(commitHash)); + } + + public void commit(TestCommit[] commits) throws IOException, GitAPIException { + for (TestCommit commit : commits) { + this.commit(commit); + } + } + + public RevCommit commit(TestCommit commit) throws IOException, GitAPIException { + + final String branch = commit.getBranch().orElse("master"); + final PersonIdent author; + + if (commit.getAuthorDate().isPresent()) { + Date date = Date.from(commit.getAuthorDate().get()); + author = new PersonIdent("peter", "peter@email.com", date, TimeZone.getDefault()); + } else { + author = new PersonIdent("peter", "peter@gmail.com"); + } + + // Checkout correct branch + try (Git git = Git.open(repoDir.toFile())) { + if (!currentBranch.equals(branch)) { + if (branches.contains(branch)) { + // branch already exists + git.checkout().setName(branch).call(); + } else { + // new branch + git.checkout().setName(branch).setCreateBranch(true).call(); + branches.add(branch); + } + currentBranch = branch; + } + + // Write file and stage it + Files.write( + repoDir.resolve(commit.getFile()), + commit.getContent().getBytes(), + StandardOpenOption.CREATE); + + git.add().addFilepattern(commit.getFile()).call(); + + // Create the commit + RevCommit revCommit = + git.commit() + .setAuthor(author) + .setCommitter("peter", "peter@email.com") + .setMessage(commit.getMessage()) + .call(); + + this.commitHashMap.put(commit, revCommit.getId().getName()); + this.revCommitMap.put(commit, revCommit); + this.revByHashMap.put(revCommit.getId().getName(), revCommit); + + return revCommit; + } + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/AccessUtilsTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/AccessUtilsTest.java index 2b0f3eb96..5ead82242 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/AccessUtilsTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/AccessUtilsTest.java @@ -12,36 +12,36 @@ class AccessUtilsTest { - @Test - void readSource() { - RepoId repoId = new RepoId(); - String repoIdStr = repoId.getIdAsString(); - CommitHash commitHash = new CommitHash("6121c30f8d73ae7f6a19bad8fd6bcb8379cf2287"); - String commitHashStr = commitHash.getHash(); - String tarDesc = "tarDesc"; + @Test + void readSource() { + RepoId repoId = new RepoId(); + String repoIdStr = repoId.getIdAsString(); + CommitHash commitHash = new CommitHash("6121c30f8d73ae7f6a19bad8fd6bcb8379cf2287"); + String commitHashStr = commitHash.getHash(); + String tarDesc = "tarDesc"; - assertThatThrownBy(() -> AccessUtils.readSource(null, null, null)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> AccessUtils.readSource(null, null, null)) + .isInstanceOf(IllegalArgumentException.class); - assertThat(AccessUtils.readSource(null, null, tarDesc)) - .isEqualTo(Either.ofRight(new TarSource(tarDesc, null))); + assertThat(AccessUtils.readSource(null, null, tarDesc)) + .isEqualTo(Either.ofRight(new TarSource(tarDesc, null))); - assertThatThrownBy(() -> AccessUtils.readSource(null, commitHashStr, null)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> AccessUtils.readSource(null, commitHashStr, null)) + .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> AccessUtils.readSource(null, commitHashStr, tarDesc)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> AccessUtils.readSource(null, commitHashStr, tarDesc)) + .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> AccessUtils.readSource(repoIdStr, null, null)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> AccessUtils.readSource(repoIdStr, null, null)) + .isInstanceOf(IllegalArgumentException.class); - assertThat(AccessUtils.readSource(repoIdStr, null, tarDesc)) - .isEqualTo(Either.ofRight(new TarSource(tarDesc, repoId))); + assertThat(AccessUtils.readSource(repoIdStr, null, tarDesc)) + .isEqualTo(Either.ofRight(new TarSource(tarDesc, repoId))); - assertThat(AccessUtils.readSource(repoIdStr, commitHashStr, null)) - .isEqualTo(Either.ofLeft(new CommitSource(repoId, commitHash))); + assertThat(AccessUtils.readSource(repoIdStr, commitHashStr, null)) + .isEqualTo(Either.ofLeft(new CommitSource(repoId, commitHash))); - assertThatThrownBy(() -> AccessUtils.readSource(repoIdStr, commitHashStr, tarDesc)) - .isInstanceOf(IllegalArgumentException.class); - } + assertThatThrownBy(() -> AccessUtils.readSource(repoIdStr, commitHashStr, tarDesc)) + .isInstanceOf(IllegalArgumentException.class); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/GitCloneTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/GitCloneTest.java index 5b9f0db7a..fd51a0dbc 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/GitCloneTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/GitCloneTest.java @@ -16,96 +16,92 @@ import org.junit.jupiter.api.io.TempDir; import org.slf4j.LoggerFactory; -/** - * Tests jgit's behaviour when cloning repos with submodules. - */ +/** Tests jgit's behaviour when cloning repos with submodules. */ class GitCloneTest { - @TempDir - Path tempDir; - - private Path repoPath; - private Path submodulePath; - - @BeforeEach - void setUp() throws Exception { - ((Logger) LoggerFactory.getLogger("org.eclipse.jgit")).setLevel(Level.INFO); - - Files.createDirectories(tempDir); - repoPath = tempDir.resolve("repo"); - submodulePath = tempDir.resolve("sub_module"); - - // Init repos - Git.init().setDirectory(repoPath.toFile()).call(); - Git.init().setDirectory(submodulePath.toFile()).call(); - - // Write file to submodule - Files.writeString(submodulePath.resolve("test.txt"), "Version 1"); - - // Commit it - Git subGit = Git.open(submodulePath.toFile()); - disableGc(subGit); - subGit.add().addFilepattern("test.txt").call(); - subGit.commit().setMessage("Hey").setAuthor("Aith", "er").call(); - - // Add submodule to repo - Git repoGit = Git.open(repoPath.toFile()); - disableGc(repoGit); - - repoGit - .submoduleAdd() - .setName("Submodule") - .setPath("submodule") - .setURI(submodulePath.toUri().toString()) - .call(); - - repoGit.commit().setAuthor("Auth", "er").setMessage("Init").call(); - - // Modify submodule - Files.writeString(submodulePath.resolve("test.txt"), "Version 2!"); - subGit.add().addFilepattern("test.txt").call(); - subGit.commit().setMessage("Updated version").setAuthor("Author", "dude").call(); - - // Update submodule in original repo - repoGit.submoduleSync().call(); - SubmoduleWalk submoduleWalk = SubmoduleWalk.forIndex(repoGit.getRepository()); - while (submoduleWalk.next()) { - Git.wrap(submoduleWalk.getRepository()).pull().setStrategy(MergeStrategy.THEIRS).call(); - } - repoGit.add().addFilepattern("submodule").call(); - // Commit modification - repoGit.commit().setAuthor("Auth", "er").setMessage("Updated submodule").call(); - - Git.init().setDirectory(tempDir.resolve("bench_repo").toFile()).call(); - } - - private void disableGc(Git repoGit) throws IOException { - StoredConfig config = repoGit.getRepository().getConfig(); - config.setInt("gc", null, "auto", 0); - config.save(); - } - - @Test - void verifySubmodulesClonedCorrectlyInOriginal() throws Exception { - assertThat(Git.open(repoPath.toFile()).status().call().isClean()) - .withFailMessage("The repo was not clean!") - .isTrue(); - assertThat(Files.readString(repoPath.resolve("submodule/test.txt"))) - .isEqualTo("Version 2!"); - } - - @Test - void checkoutCorrectSubmoduleVersion() throws Exception { - Git repo = Git.open(repoPath.toFile()); - repo.checkout() - .setName(repo.getRepository().resolve("HEAD^").toObjectId().getName()) - .setForced(true) - .call(); - - repo.submoduleSync().call(); - repo.submoduleUpdate().setStrategy(MergeStrategy.THEIRS).setFetch(true).call(); - - assertThat(Files.readString(repoPath.resolve("submodule").resolve("test.txt"))) - .isEqualTo("Version 1"); - } + @TempDir Path tempDir; + + private Path repoPath; + private Path submodulePath; + + @BeforeEach + void setUp() throws Exception { + ((Logger) LoggerFactory.getLogger("org.eclipse.jgit")).setLevel(Level.INFO); + + Files.createDirectories(tempDir); + repoPath = tempDir.resolve("repo"); + submodulePath = tempDir.resolve("sub_module"); + + // Init repos + Git.init().setDirectory(repoPath.toFile()).call(); + Git.init().setDirectory(submodulePath.toFile()).call(); + + // Write file to submodule + Files.writeString(submodulePath.resolve("test.txt"), "Version 1"); + + // Commit it + Git subGit = Git.open(submodulePath.toFile()); + disableGc(subGit); + subGit.add().addFilepattern("test.txt").call(); + subGit.commit().setMessage("Hey").setAuthor("Aith", "er").call(); + + // Add submodule to repo + Git repoGit = Git.open(repoPath.toFile()); + disableGc(repoGit); + + repoGit + .submoduleAdd() + .setName("Submodule") + .setPath("submodule") + .setURI(submodulePath.toUri().toString()) + .call(); + + repoGit.commit().setAuthor("Auth", "er").setMessage("Init").call(); + + // Modify submodule + Files.writeString(submodulePath.resolve("test.txt"), "Version 2!"); + subGit.add().addFilepattern("test.txt").call(); + subGit.commit().setMessage("Updated version").setAuthor("Author", "dude").call(); + + // Update submodule in original repo + repoGit.submoduleSync().call(); + SubmoduleWalk submoduleWalk = SubmoduleWalk.forIndex(repoGit.getRepository()); + while (submoduleWalk.next()) { + Git.wrap(submoduleWalk.getRepository()).pull().setStrategy(MergeStrategy.THEIRS).call(); + } + repoGit.add().addFilepattern("submodule").call(); + // Commit modification + repoGit.commit().setAuthor("Auth", "er").setMessage("Updated submodule").call(); + + Git.init().setDirectory(tempDir.resolve("bench_repo").toFile()).call(); + } + + private void disableGc(Git repoGit) throws IOException { + StoredConfig config = repoGit.getRepository().getConfig(); + config.setInt("gc", null, "auto", 0); + config.save(); + } + + @Test + void verifySubmodulesClonedCorrectlyInOriginal() throws Exception { + assertThat(Git.open(repoPath.toFile()).status().call().isClean()) + .withFailMessage("The repo was not clean!") + .isTrue(); + assertThat(Files.readString(repoPath.resolve("submodule/test.txt"))).isEqualTo("Version 2!"); + } + + @Test + void checkoutCorrectSubmoduleVersion() throws Exception { + Git repo = Git.open(repoPath.toFile()); + repo.checkout() + .setName(repo.getRepository().resolve("HEAD^").toObjectId().getName()) + .setForced(true) + .call(); + + repo.submoduleSync().call(); + repo.submoduleUpdate().setStrategy(MergeStrategy.THEIRS).setFetch(true).call(); + + assertThat(Files.readString(repoPath.resolve("submodule").resolve("test.txt"))) + .isEqualTo("Version 1"); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccessTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccessTest.java index ed1e89f9a..a5c2f570a 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccessTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkReadAccessTest.java @@ -24,7 +24,6 @@ import de.aaaaaaah.velcom.backend.access.repoaccess.entities.RepoId; import de.aaaaaaah.velcom.backend.storage.db.DatabaseStorage; import de.aaaaaaah.velcom.shared.util.Either; -import de.aaaaaaah.velcom.shared.util.Pair; import java.nio.file.Path; import java.time.Instant; import java.util.List; @@ -36,398 +35,504 @@ class BenchmarkReadAccessTest { - /* - * This test db setup is at follows: - * - * - Run 1 (Tar) - * - Run 2 (Tar, bench error) - * - Repo 1 - * - Run 3 (Tar) - * - Run 4 (Tar, velcom error) - * - Commit 1 - * - Run 5 - * - Run 6 - * - Run 7 (bench error) - * - Commit 2 - * - Run 8 - * - Commit 3 - * - Repo 2 - * - Run 9 (Tar) - * - Commit 4 - * - Run 10 - * - * The runs occurred in the order 1, 5, 6, 3, 9, 10, 4, 8, 2, 7 - * Each task is as many seconds long as its number. - * - * Run 5 contains successful measurements for the dimensions hello.world and test.this. - * Run 6 contains a successful measurement for hello.world and a failed measurement for test.this. - * Run 7 contains a bench script error. - * Run 4 contains a velcom error. - * - * That setup should ensure that a wide variety of edge cases can be covered. - */ - - private static final RepoId REPO1_ID = new RepoId(); - private static final RepoId REPO2_ID = new RepoId(); - - private static final CommitHash COMMIT1_HASH = - new CommitHash("bc42262ad5c504587bc4ccecab9f3f701a474047"); - private static final CommitHash COMMIT2_HASH = - new CommitHash("809170709d5f73cb273d5512a10895c10d6dc539"); - private static final CommitHash COMMIT3_HASH = - new CommitHash("cf3b1b977b01f255c6b79aee55f1d0bb77f8417b"); - private static final CommitHash COMMIT4_HASH = - new CommitHash("2aa75b3d92046e3d9c080ee8397cd3fbafea0021"); - - private static final RunId RUN1_ID = RunId.fromString("99ff038a-cf26-4a5f-8d0f-5bc9673e6e19"); - private static final RunId RUN2_ID = RunId.fromString("1862ce3a-0eb2-45ee-8793-e017baee0888"); - private static final RunId RUN3_ID = RunId.fromString("0fca4c5b-97c1-422e-acff-365cb0c9a608"); - private static final RunId RUN4_ID = RunId.fromString("577a7d6e-e38f-4123-a7c3-e1e5255019af"); - private static final RunId RUN5_ID = RunId.fromString("5414e886-3196-44ee-b80b-9e3146e9e903"); - private static final RunId RUN6_ID = RunId.fromString("2de7c555-6327-4b50-a826-6c26ebd8cfc3"); - private static final RunId RUN7_ID = RunId.fromString("4b5e1f15-770e-4c06-8828-14a7080346d2"); - private static final RunId RUN8_ID = RunId.fromString("e19a269b-c664-4313-a592-b20de69bccb1"); - private static final RunId RUN9_ID = RunId.fromString("6726d82b-1012-43e3-b2e1-1a1dd56915d1"); - private static final RunId RUN10_ID = RunId.fromString("d505ff5f-7591-4208-a491-ee807621ddd7"); - private static final Instant RUN1_START = Instant.ofEpochSecond(1600010001); - private static final Instant RUN5_START = Instant.ofEpochSecond(1600020001); - private static final Instant RUN6_START = Instant.ofEpochSecond(1600030001); - private static final Instant RUN3_START = Instant.ofEpochSecond(1600040001); - private static final Instant RUN9_START = Instant.ofEpochSecond(1600050001); - private static final Instant RUN10_START = Instant.ofEpochSecond(1600060001); - private static final Instant RUN4_START = Instant.ofEpochSecond(1600070001); - private static final Instant RUN8_START = Instant.ofEpochSecond(1600080001); - private static final Instant RUN2_START = Instant.ofEpochSecond(1600090001); - private static final Instant RUN7_START = Instant.ofEpochSecond(1600100001); - - private static final Dimension DIM_HW = new Dimension("hello", "world"); - private static final Dimension DIM_TT = new Dimension("test", "this"); - - private BenchmarkReadAccess access; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - TestDb testDb = new TestDb(tempDir); - - testDb.addRepo(REPO1_ID); - testDb.addRepo(REPO2_ID); - testDb.addCommit(REPO1_ID, COMMIT1_HASH, true, true, true, "blad9bla", - Instant.ofEpochSecond(1), Instant.ofEpochSecond(1)); - testDb.addCommit(REPO1_ID, COMMIT2_HASH, true, true, true, "m2", - Instant.ofEpochSecond(2), Instant.ofEpochSecond(2)); - testDb.addCommit(REPO1_ID, COMMIT3_HASH, true, true, true, "m3", - Instant.ofEpochSecond(3), Instant.ofEpochSecond(3)); - testDb.addCommit(REPO2_ID, COMMIT4_HASH, true, true, true, "m4", - Instant.ofEpochSecond(4), Instant.ofEpochSecond(4)); - - testDb.addRun(RUN1_ID, "a1", "rn1", "ri1", RUN1_START, RUN1_START.plusSeconds(1), - Either.ofRight(new TarSource("td1", null)), null); - testDb.addRun(RUN2_ID, "a2", "rn2", "ri2", RUN2_START, RUN2_START.plusSeconds(2), - Either.ofRight(new TarSource("td2", null)), - new RunError("em2", RunErrorType.BENCH_SCRIPT_ERROR)); - testDb.addRun(RUN3_ID, "a3", "rn3", "ri3", RUN3_START, RUN3_START.plusSeconds(3), - Either.ofRight(new TarSource("td3", REPO1_ID)), null); - testDb.addRun(RUN4_ID, "a4", "rn4", "ri4", RUN4_START, RUN4_START.plusSeconds(4), - Either.ofRight(new TarSource("td4", REPO1_ID)), - new RunError("em4", RunErrorType.VELCOM_ERROR)); - testDb.addRun(RUN5_ID, "a5", "rn5", "ri5", RUN5_START, RUN5_START.plusSeconds(5), - Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH)), null); - testDb.addRun(RUN6_ID, "a6", "rn6", "ri6", RUN6_START, RUN6_START.plusSeconds(6), - Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH)), null); - testDb.addRun(RUN7_ID, "a7", "rn7", "ri7", RUN7_START, RUN7_START.plusSeconds(7), - Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH)), - new RunError("em7", RunErrorType.BENCH_SCRIPT_ERROR)); - testDb.addRun(RUN8_ID, "a8", "rn8", "ri8", RUN8_START, RUN8_START.plusSeconds(8), - Either.ofLeft(new CommitSource(REPO1_ID, COMMIT2_HASH)), null); - testDb.addRun(RUN9_ID, "a9", "rn9", "ri9", RUN9_START, RUN9_START.plusSeconds(9), - Either.ofRight(new TarSource("td9", REPO2_ID)), null); - testDb.addRun(RUN10_ID, "a10", "rn10", "ri10", RUN10_START, RUN10_START.plusSeconds(10), - Either.ofLeft(new CommitSource(REPO2_ID, COMMIT4_HASH)), null); - - // Required for foreign key constraints - testDb.addDimension(DIM_HW); - testDb.addDimension(DIM_TT); - - testDb.addMeasurement(RUN5_ID, DIM_HW, null, Interpretation.LESS_IS_BETTER, - Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d, 4d, 5d)))); - testDb.addMeasurement(RUN5_ID, DIM_TT, new Unit("asdf"), null, - Either.ofRight(new MeasurementValues(List.of(6d, 7d, 8d, 9d, 10d, 11d, 12d, 13d, 14d, 15d)))); - testDb.addMeasurement(RUN6_ID, DIM_HW, null, null, - Either.ofRight(new MeasurementValues(List.of(16d)))); - testDb.addMeasurement(RUN6_ID, DIM_TT, new Unit("xyz"), Interpretation.MORE_IS_BETTER, - Either.ofLeft(new MeasurementError("blargh"))); - - DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - access = new BenchmarkReadAccess(databaseStorage); - } - - @Test - void getAllRunIds() { - assertThat(access.getAllRunIds(REPO1_ID, COMMIT1_HASH)) - .containsExactlyInAnyOrder(RUN5_ID, RUN6_ID, RUN7_ID); - assertThat(access.getAllRunIds(REPO1_ID, COMMIT2_HASH)).containsExactlyInAnyOrder(RUN8_ID); - assertThat(access.getAllRunIds(REPO1_ID, COMMIT3_HASH)).isEmpty(); - assertThat(access.getAllRunIds(REPO2_ID, COMMIT4_HASH)).containsExactlyInAnyOrder(RUN10_ID); - - assertThat(access.getAllRunIds(REPO1_ID, COMMIT4_HASH)).isEmpty(); - assertThat(access.getAllRunIds(REPO2_ID, COMMIT1_HASH)).isEmpty(); - } - - @Test - void getRecentRuns() { - // Run order from old to new: 1, 5, 6, 3, 9, 10, 4, 8, 2, 7 - // Run order from new to old: 7, 2, 8, 4, 10, 9, 3, 6, 5, 1 - // Excluding tar runs: 7, 8, 10, 6, 5 - // Grouped by commits: 10, 8, 7, 6, 5 - - assertThat(access.getRecentRunIds(0, 10)) - .containsExactly(RUN10_ID, RUN8_ID, RUN7_ID, RUN6_ID, RUN5_ID); - - // Get varying amounts at offset 0 - - assertThat(access.getRecentRunIds(0, 20)) - .containsExactly(RUN10_ID, RUN8_ID, RUN7_ID, RUN6_ID, RUN5_ID); - assertThat(access.getRecentRunIds(0, 3)) - .containsExactly(RUN10_ID, RUN8_ID, RUN7_ID); - assertThat(access.getRecentRunIds(0, 1)).containsExactly(RUN10_ID); - assertThat(access.getRecentRunIds(0, 0)).isEmpty(); - - // Get varying amounts at varying offsets - - assertThat(access.getRecentRunIds(2, 4)).containsExactly(RUN7_ID, RUN6_ID, RUN5_ID); - assertThat(access.getRecentRunIds(4, 3)).containsExactly(RUN5_ID); - assertThat(access.getRecentRunIds(5, 3)).isEmpty(); - - // Illegal arguments - - assertThatThrownBy(() -> access.getRecentRunIds(-1, 10)) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> access.getRecentRunIds(0, -1)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - void getLatestRuns() { - assertThat(access.getLatestRunId(REPO1_ID, COMMIT1_HASH)).isEqualTo(Optional.of(RUN7_ID)); - assertThat(access.getLatestRunId(REPO1_ID, COMMIT2_HASH)).isEqualTo(Optional.of(RUN8_ID)); - assertThat(access.getLatestRunId(REPO1_ID, COMMIT3_HASH)).isEqualTo(Optional.empty()); - assertThat(access.getLatestRunId(REPO2_ID, COMMIT4_HASH)).isEqualTo(Optional.of(RUN10_ID)); - - assertThat(access.getLatestRunId(REPO2_ID, COMMIT1_HASH)).isEqualTo(Optional.empty()); - assertThat(access.getLatestRunId(REPO2_ID, COMMIT2_HASH)).isEqualTo(Optional.empty()); - assertThat(access.getLatestRunId(REPO2_ID, COMMIT3_HASH)).isEqualTo(Optional.empty()); - assertThat(access.getLatestRunId(REPO1_ID, COMMIT4_HASH)).isEqualTo(Optional.empty()); - - Map latestRunIds = access - .getLatestRunIds(REPO1_ID, List.of(COMMIT1_HASH, COMMIT3_HASH, COMMIT4_HASH)); - - assertThat(latestRunIds).containsOnlyKeys(COMMIT1_HASH); - assertThat(latestRunIds.get(COMMIT1_HASH)).isEqualTo(RUN7_ID); - - latestRunIds = access - .getLatestRunIds(REPO1_ID, List.of(COMMIT1_HASH, COMMIT2_HASH, COMMIT3_HASH)); - - assertThat(latestRunIds).containsOnlyKeys(COMMIT1_HASH, COMMIT2_HASH); - assertThat(latestRunIds.get(COMMIT1_HASH)).isEqualTo(RUN7_ID); - assertThat(latestRunIds.get(COMMIT2_HASH)).isEqualTo(RUN8_ID); - } - - @Test - void getRuns() { - Run run4 = access.getRun(RUN4_ID); - assertThat(run4.getId()).isEqualTo(RUN4_ID); - assertThat(run4.getAuthor()).isEqualTo("a4"); - assertThat(run4.getRunnerName()).isEqualTo("rn4"); - assertThat(run4.getRunnerInfo()).isEqualTo("ri4"); - assertThat(run4.getStartTime()).isEqualTo(RUN4_START); - assertThat(run4.getStopTime()).isEqualTo(RUN4_START.plusSeconds(4)); - assertThat(run4.getSource()).isEqualTo(Either.ofRight(new TarSource("td4", REPO1_ID))); - assertThat(run4.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run4.getResult()) - .isEqualTo(Either.ofLeft(new RunError("em4", RunErrorType.VELCOM_ERROR))); - assertThat(run4.getAllDimensionsUsed()).isEmpty(); - - Run run5 = access.getRun(RUN5_ID); - assertThat(run5.getId()).isEqualTo(RUN5_ID); - assertThat(run5.getAuthor()).isEqualTo("a5"); - assertThat(run5.getRunnerName()).isEqualTo("rn5"); - assertThat(run5.getRunnerInfo()).isEqualTo("ri5"); - assertThat(run5.getStartTime()).isEqualTo(RUN5_START); - assertThat(run5.getStopTime()).isEqualTo(RUN5_START.plusSeconds(5)); - assertThat(run5.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); - assertThat(run5.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run5.getResult().getRight()).isPresent(); - assertThat(run5.getResult().getRight().get()).containsExactlyInAnyOrder( - new Measurement(RUN5_ID, DIM_HW, - Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d, 4d, 5d)))), - new Measurement(RUN5_ID, DIM_TT, Either - .ofRight(new MeasurementValues(List.of(6d, 7d, 8d, 9d, 10d, 11d, 12d, 13d, 14d, 15d)))) - ); - assertThat(run5.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); - - Run run6 = access.getRun(RUN6_ID); - assertThat(run6.getId()).isEqualTo(RUN6_ID); - assertThat(run6.getAuthor()).isEqualTo("a6"); - assertThat(run6.getRunnerName()).isEqualTo("rn6"); - assertThat(run6.getRunnerInfo()).isEqualTo("ri6"); - assertThat(run6.getStartTime()).isEqualTo(RUN6_START); - assertThat(run6.getStopTime()).isEqualTo(RUN6_START.plusSeconds(6)); - assertThat(run6.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); - assertThat(run6.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run6.getResult().getRight()).isPresent(); - assertThat(run6.getResult().getRight().get()).containsExactlyInAnyOrder( - new Measurement(RUN6_ID, DIM_HW, Either.ofRight(new MeasurementValues(List.of(16d)))), - new Measurement(RUN6_ID, DIM_TT, Either.ofLeft(new MeasurementError("blargh"))) - ); - assertThat(run6.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); - - Run run7 = access.getRun(RUN7_ID); - assertThat(run7.getId()).isEqualTo(RUN7_ID); - assertThat(run7.getAuthor()).isEqualTo("a7"); - assertThat(run7.getRunnerName()).isEqualTo("rn7"); - assertThat(run7.getRunnerInfo()).isEqualTo("ri7"); - assertThat(run7.getStartTime()).isEqualTo(RUN7_START); - assertThat(run7.getStopTime()).isEqualTo(RUN7_START.plusSeconds(7)); - assertThat(run7.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); - assertThat(run7.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run7.getResult()) - .isEqualTo(Either.ofLeft(new RunError("em7", RunErrorType.BENCH_SCRIPT_ERROR))); - assertThat(run7.getAllDimensionsUsed()).isEmpty(); - - RunId nonexistentId = new RunId(); - - assertThatThrownBy(() -> access.getRun(nonexistentId)) - .isInstanceOf(NoSuchRunException.class) - .extracting("invalidSource") - .isEqualTo(Either.ofLeft(nonexistentId)); - - List runs = access.getRuns(List.of(RUN4_ID, RUN5_ID, RUN6_ID, RUN7_ID, nonexistentId)); - Map runMap = runs.stream() - .collect(toMap(Run::getId, it -> it)); - assertThat(runMap).containsOnlyKeys(RUN4_ID, RUN5_ID, RUN6_ID, RUN7_ID); - - run4 = runMap.get(RUN4_ID); - assertThat(run4.getId()).isEqualTo(RUN4_ID); - assertThat(run4.getAuthor()).isEqualTo("a4"); - assertThat(run4.getRunnerName()).isEqualTo("rn4"); - assertThat(run4.getRunnerInfo()).isEqualTo("ri4"); - assertThat(run4.getStartTime()).isEqualTo(RUN4_START); - assertThat(run4.getStopTime()).isEqualTo(RUN4_START.plusSeconds(4)); - assertThat(run4.getSource()).isEqualTo(Either.ofRight(new TarSource("td4", REPO1_ID))); - assertThat(run4.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run4.getResult()) - .isEqualTo(Either.ofLeft(new RunError("em4", RunErrorType.VELCOM_ERROR))); - assertThat(run4.getAllDimensionsUsed()).isEmpty(); - - run5 = runMap.get(RUN5_ID); - assertThat(run5.getId()).isEqualTo(RUN5_ID); - assertThat(run5.getAuthor()).isEqualTo("a5"); - assertThat(run5.getRunnerName()).isEqualTo("rn5"); - assertThat(run5.getRunnerInfo()).isEqualTo("ri5"); - assertThat(run5.getStartTime()).isEqualTo(RUN5_START); - assertThat(run5.getStopTime()).isEqualTo(RUN5_START.plusSeconds(5)); - assertThat(run5.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); - assertThat(run5.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run5.getResult().getRight()).isPresent(); - assertThat(run5.getResult().getRight().get()).containsExactlyInAnyOrder( - new Measurement(RUN5_ID, DIM_HW, - Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d, 4d, 5d)))), - new Measurement(RUN5_ID, DIM_TT, Either - .ofRight(new MeasurementValues(List.of(6d, 7d, 8d, 9d, 10d, 11d, 12d, 13d, 14d, 15d)))) - ); - assertThat(run5.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); - - run6 = runMap.get(RUN6_ID); - assertThat(run6.getId()).isEqualTo(RUN6_ID); - assertThat(run6.getAuthor()).isEqualTo("a6"); - assertThat(run6.getRunnerName()).isEqualTo("rn6"); - assertThat(run6.getRunnerInfo()).isEqualTo("ri6"); - assertThat(run6.getStartTime()).isEqualTo(RUN6_START); - assertThat(run6.getStopTime()).isEqualTo(RUN6_START.plusSeconds(6)); - assertThat(run6.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); - assertThat(run6.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run6.getResult().getRight()).isPresent(); - assertThat(run6.getResult().getRight().get()).containsExactlyInAnyOrder( - new Measurement(RUN6_ID, DIM_HW, Either.ofRight(new MeasurementValues(List.of(16d)))), - new Measurement(RUN6_ID, DIM_TT, Either.ofLeft(new MeasurementError("blargh"))) - ); - assertThat(run6.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); - - run7 = runMap.get(RUN7_ID); - assertThat(run7.getId()).isEqualTo(RUN7_ID); - assertThat(run7.getAuthor()).isEqualTo("a7"); - assertThat(run7.getRunnerName()).isEqualTo("rn7"); - assertThat(run7.getRunnerInfo()).isEqualTo("ri7"); - assertThat(run7.getStartTime()).isEqualTo(RUN7_START); - assertThat(run7.getStopTime()).isEqualTo(RUN7_START.plusSeconds(7)); - assertThat(run7.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); - assertThat(run7.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); - assertThat(run7.getResult()) - .isEqualTo(Either.ofLeft(new RunError("em7", RunErrorType.BENCH_SCRIPT_ERROR))); - assertThat(run7.getAllDimensionsUsed()).isEmpty(); - } - - @Test - void searchRuns() { - // Search for all runs ("a" appears in every run's author) - - assertThat(access.searchRuns(100, null, "a") - .stream() - .map(SearchRunDescription::getId)) - .containsExactly(RUN7_ID, RUN2_ID, RUN8_ID, RUN4_ID, RUN10_ID, RUN9_ID, RUN3_ID, RUN6_ID, - RUN5_ID, RUN1_ID); - - // Restricted to repos - - assertThat(access.searchRuns(100, REPO1_ID, "a") - .stream() - .map(SearchRunDescription::getId)) - .containsExactly(RUN7_ID, RUN8_ID, RUN4_ID, RUN3_ID, RUN6_ID, RUN5_ID); - - assertThat(access.searchRuns(100, REPO2_ID, "a") - .stream() - .map(SearchRunDescription::getId)) - .containsExactly(RUN10_ID, RUN9_ID); - - // Commit description and hash are ignored - - assertThat(access.searchRuns(100, null, "d9") - .stream() - .map(SearchRunDescription::getId)) - .containsExactly(RUN9_ID); - - assertThat(access.searchRuns(100, null, COMMIT1_HASH.getHash())) - .isEmpty(); - - // Limit is respected - - assertThat(access.searchRuns(4, null, "a") - .stream() - .map(SearchRunDescription::getId)) - .containsExactly(RUN7_ID, RUN2_ID, RUN8_ID, RUN4_ID); - } - - @Test - void getShortRunDescription() { - assertThat(access.getShortRunDescription(RUN1_ID)) - .isEqualTo(new ShortRunDescription(RUN1_ID, null, null, "td1")); - assertThat(access.getShortRunDescription(RUN2_ID)) - .isEqualTo(new ShortRunDescription(RUN2_ID, null, null, "td2")); - assertThat(access.getShortRunDescription(RUN3_ID)) - .isEqualTo(new ShortRunDescription(RUN3_ID, null, null, "td3")); - assertThat(access.getShortRunDescription(RUN4_ID)) - .isEqualTo(new ShortRunDescription(RUN4_ID, null, null, "td4")); - assertThat(access.getShortRunDescription(RUN5_ID)) - .isEqualTo(new ShortRunDescription(RUN5_ID, COMMIT1_HASH.getHash(), "blad9bla", null)); - assertThat(access.getShortRunDescription(RUN6_ID)) - .isEqualTo(new ShortRunDescription(RUN6_ID, COMMIT1_HASH.getHash(), "blad9bla", null)); - assertThat(access.getShortRunDescription(RUN7_ID)) - .isEqualTo(new ShortRunDescription(RUN7_ID, COMMIT1_HASH.getHash(), "blad9bla", null)); - assertThat(access.getShortRunDescription(RUN8_ID)) - .isEqualTo(new ShortRunDescription(RUN8_ID, COMMIT2_HASH.getHash(), "m2", null)); - assertThat(access.getShortRunDescription(RUN9_ID)) - .isEqualTo(new ShortRunDescription(RUN9_ID, null, null, "td9")); - assertThat(access.getShortRunDescription(RUN10_ID)) - .isEqualTo(new ShortRunDescription(RUN10_ID, COMMIT4_HASH.getHash(), "m4", null)); - } + /* + * This test db setup is at follows: + * + * - Run 1 (Tar) + * - Run 2 (Tar, bench error) + * - Repo 1 + * - Run 3 (Tar) + * - Run 4 (Tar, velcom error) + * - Commit 1 + * - Run 5 + * - Run 6 + * - Run 7 (bench error) + * - Commit 2 + * - Run 8 + * - Commit 3 + * - Repo 2 + * - Run 9 (Tar) + * - Commit 4 + * - Run 10 + * + * The runs occurred in the order 1, 5, 6, 3, 9, 10, 4, 8, 2, 7 + * Each task is as many seconds long as its number. + * + * Run 5 contains successful measurements for the dimensions hello.world and test.this. + * Run 6 contains a successful measurement for hello.world and a failed measurement for test.this. + * Run 7 contains a bench script error. + * Run 4 contains a velcom error. + * + * That setup should ensure that a wide variety of edge cases can be covered. + */ + + private static final RepoId REPO1_ID = new RepoId(); + private static final RepoId REPO2_ID = new RepoId(); + + private static final CommitHash COMMIT1_HASH = + new CommitHash("bc42262ad5c504587bc4ccecab9f3f701a474047"); + private static final CommitHash COMMIT2_HASH = + new CommitHash("809170709d5f73cb273d5512a10895c10d6dc539"); + private static final CommitHash COMMIT3_HASH = + new CommitHash("cf3b1b977b01f255c6b79aee55f1d0bb77f8417b"); + private static final CommitHash COMMIT4_HASH = + new CommitHash("2aa75b3d92046e3d9c080ee8397cd3fbafea0021"); + + private static final RunId RUN1_ID = RunId.fromString("99ff038a-cf26-4a5f-8d0f-5bc9673e6e19"); + private static final RunId RUN2_ID = RunId.fromString("1862ce3a-0eb2-45ee-8793-e017baee0888"); + private static final RunId RUN3_ID = RunId.fromString("0fca4c5b-97c1-422e-acff-365cb0c9a608"); + private static final RunId RUN4_ID = RunId.fromString("577a7d6e-e38f-4123-a7c3-e1e5255019af"); + private static final RunId RUN5_ID = RunId.fromString("5414e886-3196-44ee-b80b-9e3146e9e903"); + private static final RunId RUN6_ID = RunId.fromString("2de7c555-6327-4b50-a826-6c26ebd8cfc3"); + private static final RunId RUN7_ID = RunId.fromString("4b5e1f15-770e-4c06-8828-14a7080346d2"); + private static final RunId RUN8_ID = RunId.fromString("e19a269b-c664-4313-a592-b20de69bccb1"); + private static final RunId RUN9_ID = RunId.fromString("6726d82b-1012-43e3-b2e1-1a1dd56915d1"); + private static final RunId RUN10_ID = RunId.fromString("d505ff5f-7591-4208-a491-ee807621ddd7"); + private static final Instant RUN1_START = Instant.ofEpochSecond(1600010001); + private static final Instant RUN5_START = Instant.ofEpochSecond(1600020001); + private static final Instant RUN6_START = Instant.ofEpochSecond(1600030001); + private static final Instant RUN3_START = Instant.ofEpochSecond(1600040001); + private static final Instant RUN9_START = Instant.ofEpochSecond(1600050001); + private static final Instant RUN10_START = Instant.ofEpochSecond(1600060001); + private static final Instant RUN4_START = Instant.ofEpochSecond(1600070001); + private static final Instant RUN8_START = Instant.ofEpochSecond(1600080001); + private static final Instant RUN2_START = Instant.ofEpochSecond(1600090001); + private static final Instant RUN7_START = Instant.ofEpochSecond(1600100001); + + private static final Dimension DIM_HW = new Dimension("hello", "world"); + private static final Dimension DIM_TT = new Dimension("test", "this"); + + private BenchmarkReadAccess access; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + TestDb testDb = new TestDb(tempDir); + + testDb.addRepo(REPO1_ID); + testDb.addRepo(REPO2_ID); + testDb.addCommit( + REPO1_ID, + COMMIT1_HASH, + true, + true, + true, + "blad9bla", + Instant.ofEpochSecond(1), + Instant.ofEpochSecond(1)); + testDb.addCommit( + REPO1_ID, + COMMIT2_HASH, + true, + true, + true, + "m2", + Instant.ofEpochSecond(2), + Instant.ofEpochSecond(2)); + testDb.addCommit( + REPO1_ID, + COMMIT3_HASH, + true, + true, + true, + "m3", + Instant.ofEpochSecond(3), + Instant.ofEpochSecond(3)); + testDb.addCommit( + REPO2_ID, + COMMIT4_HASH, + true, + true, + true, + "m4", + Instant.ofEpochSecond(4), + Instant.ofEpochSecond(4)); + + testDb.addRun( + RUN1_ID, + "a1", + "rn1", + "ri1", + RUN1_START, + RUN1_START.plusSeconds(1), + Either.ofRight(new TarSource("td1", null)), + null); + testDb.addRun( + RUN2_ID, + "a2", + "rn2", + "ri2", + RUN2_START, + RUN2_START.plusSeconds(2), + Either.ofRight(new TarSource("td2", null)), + new RunError("em2", RunErrorType.BENCH_SCRIPT_ERROR)); + testDb.addRun( + RUN3_ID, + "a3", + "rn3", + "ri3", + RUN3_START, + RUN3_START.plusSeconds(3), + Either.ofRight(new TarSource("td3", REPO1_ID)), + null); + testDb.addRun( + RUN4_ID, + "a4", + "rn4", + "ri4", + RUN4_START, + RUN4_START.plusSeconds(4), + Either.ofRight(new TarSource("td4", REPO1_ID)), + new RunError("em4", RunErrorType.VELCOM_ERROR)); + testDb.addRun( + RUN5_ID, + "a5", + "rn5", + "ri5", + RUN5_START, + RUN5_START.plusSeconds(5), + Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH)), + null); + testDb.addRun( + RUN6_ID, + "a6", + "rn6", + "ri6", + RUN6_START, + RUN6_START.plusSeconds(6), + Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH)), + null); + testDb.addRun( + RUN7_ID, + "a7", + "rn7", + "ri7", + RUN7_START, + RUN7_START.plusSeconds(7), + Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH)), + new RunError("em7", RunErrorType.BENCH_SCRIPT_ERROR)); + testDb.addRun( + RUN8_ID, + "a8", + "rn8", + "ri8", + RUN8_START, + RUN8_START.plusSeconds(8), + Either.ofLeft(new CommitSource(REPO1_ID, COMMIT2_HASH)), + null); + testDb.addRun( + RUN9_ID, + "a9", + "rn9", + "ri9", + RUN9_START, + RUN9_START.plusSeconds(9), + Either.ofRight(new TarSource("td9", REPO2_ID)), + null); + testDb.addRun( + RUN10_ID, + "a10", + "rn10", + "ri10", + RUN10_START, + RUN10_START.plusSeconds(10), + Either.ofLeft(new CommitSource(REPO2_ID, COMMIT4_HASH)), + null); + + // Required for foreign key constraints + testDb.addDimension(DIM_HW); + testDb.addDimension(DIM_TT); + + testDb.addMeasurement( + RUN5_ID, + DIM_HW, + null, + Interpretation.LESS_IS_BETTER, + Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d, 4d, 5d)))); + testDb.addMeasurement( + RUN5_ID, + DIM_TT, + new Unit("asdf"), + null, + Either.ofRight( + new MeasurementValues(List.of(6d, 7d, 8d, 9d, 10d, 11d, 12d, 13d, 14d, 15d)))); + testDb.addMeasurement( + RUN6_ID, DIM_HW, null, null, Either.ofRight(new MeasurementValues(List.of(16d)))); + testDb.addMeasurement( + RUN6_ID, + DIM_TT, + new Unit("xyz"), + Interpretation.MORE_IS_BETTER, + Either.ofLeft(new MeasurementError("blargh"))); + + DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + access = new BenchmarkReadAccess(databaseStorage); + } + + @Test + void getAllRunIds() { + assertThat(access.getAllRunIds(REPO1_ID, COMMIT1_HASH)) + .containsExactlyInAnyOrder(RUN5_ID, RUN6_ID, RUN7_ID); + assertThat(access.getAllRunIds(REPO1_ID, COMMIT2_HASH)).containsExactlyInAnyOrder(RUN8_ID); + assertThat(access.getAllRunIds(REPO1_ID, COMMIT3_HASH)).isEmpty(); + assertThat(access.getAllRunIds(REPO2_ID, COMMIT4_HASH)).containsExactlyInAnyOrder(RUN10_ID); + + assertThat(access.getAllRunIds(REPO1_ID, COMMIT4_HASH)).isEmpty(); + assertThat(access.getAllRunIds(REPO2_ID, COMMIT1_HASH)).isEmpty(); + } + + @Test + void getRecentRuns() { + // Run order from old to new: 1, 5, 6, 3, 9, 10, 4, 8, 2, 7 + // Run order from new to old: 7, 2, 8, 4, 10, 9, 3, 6, 5, 1 + // Excluding tar runs: 7, 8, 10, 6, 5 + // Grouped by commits: 10, 8, 7, 6, 5 + + assertThat(access.getRecentRunIds(0, 10)) + .containsExactly(RUN10_ID, RUN8_ID, RUN7_ID, RUN6_ID, RUN5_ID); + + // Get varying amounts at offset 0 + + assertThat(access.getRecentRunIds(0, 20)) + .containsExactly(RUN10_ID, RUN8_ID, RUN7_ID, RUN6_ID, RUN5_ID); + assertThat(access.getRecentRunIds(0, 3)).containsExactly(RUN10_ID, RUN8_ID, RUN7_ID); + assertThat(access.getRecentRunIds(0, 1)).containsExactly(RUN10_ID); + assertThat(access.getRecentRunIds(0, 0)).isEmpty(); + + // Get varying amounts at varying offsets + + assertThat(access.getRecentRunIds(2, 4)).containsExactly(RUN7_ID, RUN6_ID, RUN5_ID); + assertThat(access.getRecentRunIds(4, 3)).containsExactly(RUN5_ID); + assertThat(access.getRecentRunIds(5, 3)).isEmpty(); + + // Illegal arguments + + assertThatThrownBy(() -> access.getRecentRunIds(-1, 10)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> access.getRecentRunIds(0, -1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void getLatestRuns() { + assertThat(access.getLatestRunId(REPO1_ID, COMMIT1_HASH)).isEqualTo(Optional.of(RUN7_ID)); + assertThat(access.getLatestRunId(REPO1_ID, COMMIT2_HASH)).isEqualTo(Optional.of(RUN8_ID)); + assertThat(access.getLatestRunId(REPO1_ID, COMMIT3_HASH)).isEqualTo(Optional.empty()); + assertThat(access.getLatestRunId(REPO2_ID, COMMIT4_HASH)).isEqualTo(Optional.of(RUN10_ID)); + + assertThat(access.getLatestRunId(REPO2_ID, COMMIT1_HASH)).isEqualTo(Optional.empty()); + assertThat(access.getLatestRunId(REPO2_ID, COMMIT2_HASH)).isEqualTo(Optional.empty()); + assertThat(access.getLatestRunId(REPO2_ID, COMMIT3_HASH)).isEqualTo(Optional.empty()); + assertThat(access.getLatestRunId(REPO1_ID, COMMIT4_HASH)).isEqualTo(Optional.empty()); + + Map latestRunIds = + access.getLatestRunIds(REPO1_ID, List.of(COMMIT1_HASH, COMMIT3_HASH, COMMIT4_HASH)); + + assertThat(latestRunIds).containsOnlyKeys(COMMIT1_HASH); + assertThat(latestRunIds.get(COMMIT1_HASH)).isEqualTo(RUN7_ID); + + latestRunIds = + access.getLatestRunIds(REPO1_ID, List.of(COMMIT1_HASH, COMMIT2_HASH, COMMIT3_HASH)); + + assertThat(latestRunIds).containsOnlyKeys(COMMIT1_HASH, COMMIT2_HASH); + assertThat(latestRunIds.get(COMMIT1_HASH)).isEqualTo(RUN7_ID); + assertThat(latestRunIds.get(COMMIT2_HASH)).isEqualTo(RUN8_ID); + } + + @Test + void getRuns() { + Run run4 = access.getRun(RUN4_ID); + assertThat(run4.getId()).isEqualTo(RUN4_ID); + assertThat(run4.getAuthor()).isEqualTo("a4"); + assertThat(run4.getRunnerName()).isEqualTo("rn4"); + assertThat(run4.getRunnerInfo()).isEqualTo("ri4"); + assertThat(run4.getStartTime()).isEqualTo(RUN4_START); + assertThat(run4.getStopTime()).isEqualTo(RUN4_START.plusSeconds(4)); + assertThat(run4.getSource()).isEqualTo(Either.ofRight(new TarSource("td4", REPO1_ID))); + assertThat(run4.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run4.getResult()) + .isEqualTo(Either.ofLeft(new RunError("em4", RunErrorType.VELCOM_ERROR))); + assertThat(run4.getAllDimensionsUsed()).isEmpty(); + + Run run5 = access.getRun(RUN5_ID); + assertThat(run5.getId()).isEqualTo(RUN5_ID); + assertThat(run5.getAuthor()).isEqualTo("a5"); + assertThat(run5.getRunnerName()).isEqualTo("rn5"); + assertThat(run5.getRunnerInfo()).isEqualTo("ri5"); + assertThat(run5.getStartTime()).isEqualTo(RUN5_START); + assertThat(run5.getStopTime()).isEqualTo(RUN5_START.plusSeconds(5)); + assertThat(run5.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); + assertThat(run5.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run5.getResult().getRight()).isPresent(); + assertThat(run5.getResult().getRight().get()) + .containsExactlyInAnyOrder( + new Measurement( + RUN5_ID, + DIM_HW, + Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d, 4d, 5d)))), + new Measurement( + RUN5_ID, + DIM_TT, + Either.ofRight( + new MeasurementValues(List.of(6d, 7d, 8d, 9d, 10d, 11d, 12d, 13d, 14d, 15d))))); + assertThat(run5.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); + + Run run6 = access.getRun(RUN6_ID); + assertThat(run6.getId()).isEqualTo(RUN6_ID); + assertThat(run6.getAuthor()).isEqualTo("a6"); + assertThat(run6.getRunnerName()).isEqualTo("rn6"); + assertThat(run6.getRunnerInfo()).isEqualTo("ri6"); + assertThat(run6.getStartTime()).isEqualTo(RUN6_START); + assertThat(run6.getStopTime()).isEqualTo(RUN6_START.plusSeconds(6)); + assertThat(run6.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); + assertThat(run6.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run6.getResult().getRight()).isPresent(); + assertThat(run6.getResult().getRight().get()) + .containsExactlyInAnyOrder( + new Measurement(RUN6_ID, DIM_HW, Either.ofRight(new MeasurementValues(List.of(16d)))), + new Measurement(RUN6_ID, DIM_TT, Either.ofLeft(new MeasurementError("blargh")))); + assertThat(run6.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); + + Run run7 = access.getRun(RUN7_ID); + assertThat(run7.getId()).isEqualTo(RUN7_ID); + assertThat(run7.getAuthor()).isEqualTo("a7"); + assertThat(run7.getRunnerName()).isEqualTo("rn7"); + assertThat(run7.getRunnerInfo()).isEqualTo("ri7"); + assertThat(run7.getStartTime()).isEqualTo(RUN7_START); + assertThat(run7.getStopTime()).isEqualTo(RUN7_START.plusSeconds(7)); + assertThat(run7.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); + assertThat(run7.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run7.getResult()) + .isEqualTo(Either.ofLeft(new RunError("em7", RunErrorType.BENCH_SCRIPT_ERROR))); + assertThat(run7.getAllDimensionsUsed()).isEmpty(); + + RunId nonexistentId = new RunId(); + + assertThatThrownBy(() -> access.getRun(nonexistentId)) + .isInstanceOf(NoSuchRunException.class) + .extracting("invalidSource") + .isEqualTo(Either.ofLeft(nonexistentId)); + + List runs = access.getRuns(List.of(RUN4_ID, RUN5_ID, RUN6_ID, RUN7_ID, nonexistentId)); + Map runMap = runs.stream().collect(toMap(Run::getId, it -> it)); + assertThat(runMap).containsOnlyKeys(RUN4_ID, RUN5_ID, RUN6_ID, RUN7_ID); + + run4 = runMap.get(RUN4_ID); + assertThat(run4.getId()).isEqualTo(RUN4_ID); + assertThat(run4.getAuthor()).isEqualTo("a4"); + assertThat(run4.getRunnerName()).isEqualTo("rn4"); + assertThat(run4.getRunnerInfo()).isEqualTo("ri4"); + assertThat(run4.getStartTime()).isEqualTo(RUN4_START); + assertThat(run4.getStopTime()).isEqualTo(RUN4_START.plusSeconds(4)); + assertThat(run4.getSource()).isEqualTo(Either.ofRight(new TarSource("td4", REPO1_ID))); + assertThat(run4.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run4.getResult()) + .isEqualTo(Either.ofLeft(new RunError("em4", RunErrorType.VELCOM_ERROR))); + assertThat(run4.getAllDimensionsUsed()).isEmpty(); + + run5 = runMap.get(RUN5_ID); + assertThat(run5.getId()).isEqualTo(RUN5_ID); + assertThat(run5.getAuthor()).isEqualTo("a5"); + assertThat(run5.getRunnerName()).isEqualTo("rn5"); + assertThat(run5.getRunnerInfo()).isEqualTo("ri5"); + assertThat(run5.getStartTime()).isEqualTo(RUN5_START); + assertThat(run5.getStopTime()).isEqualTo(RUN5_START.plusSeconds(5)); + assertThat(run5.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); + assertThat(run5.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run5.getResult().getRight()).isPresent(); + assertThat(run5.getResult().getRight().get()) + .containsExactlyInAnyOrder( + new Measurement( + RUN5_ID, + DIM_HW, + Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d, 4d, 5d)))), + new Measurement( + RUN5_ID, + DIM_TT, + Either.ofRight( + new MeasurementValues(List.of(6d, 7d, 8d, 9d, 10d, 11d, 12d, 13d, 14d, 15d))))); + assertThat(run5.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); + + run6 = runMap.get(RUN6_ID); + assertThat(run6.getId()).isEqualTo(RUN6_ID); + assertThat(run6.getAuthor()).isEqualTo("a6"); + assertThat(run6.getRunnerName()).isEqualTo("rn6"); + assertThat(run6.getRunnerInfo()).isEqualTo("ri6"); + assertThat(run6.getStartTime()).isEqualTo(RUN6_START); + assertThat(run6.getStopTime()).isEqualTo(RUN6_START.plusSeconds(6)); + assertThat(run6.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); + assertThat(run6.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run6.getResult().getRight()).isPresent(); + assertThat(run6.getResult().getRight().get()) + .containsExactlyInAnyOrder( + new Measurement(RUN6_ID, DIM_HW, Either.ofRight(new MeasurementValues(List.of(16d)))), + new Measurement(RUN6_ID, DIM_TT, Either.ofLeft(new MeasurementError("blargh")))); + assertThat(run6.getAllDimensionsUsed()).containsExactlyInAnyOrder(DIM_HW, DIM_TT); + + run7 = runMap.get(RUN7_ID); + assertThat(run7.getId()).isEqualTo(RUN7_ID); + assertThat(run7.getAuthor()).isEqualTo("a7"); + assertThat(run7.getRunnerName()).isEqualTo("rn7"); + assertThat(run7.getRunnerInfo()).isEqualTo("ri7"); + assertThat(run7.getStartTime()).isEqualTo(RUN7_START); + assertThat(run7.getStopTime()).isEqualTo(RUN7_START.plusSeconds(7)); + assertThat(run7.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO1_ID, COMMIT1_HASH))); + assertThat(run7.getRepoId()).isEqualTo(Optional.of(REPO1_ID)); + assertThat(run7.getResult()) + .isEqualTo(Either.ofLeft(new RunError("em7", RunErrorType.BENCH_SCRIPT_ERROR))); + assertThat(run7.getAllDimensionsUsed()).isEmpty(); + } + + @Test + void searchRuns() { + // Search for all runs ("a" appears in every run's author) + + assertThat(access.searchRuns(100, null, "a").stream().map(SearchRunDescription::getId)) + .containsExactly( + RUN7_ID, RUN2_ID, RUN8_ID, RUN4_ID, RUN10_ID, RUN9_ID, RUN3_ID, RUN6_ID, RUN5_ID, + RUN1_ID); + + // Restricted to repos + + assertThat(access.searchRuns(100, REPO1_ID, "a").stream().map(SearchRunDescription::getId)) + .containsExactly(RUN7_ID, RUN8_ID, RUN4_ID, RUN3_ID, RUN6_ID, RUN5_ID); + + assertThat(access.searchRuns(100, REPO2_ID, "a").stream().map(SearchRunDescription::getId)) + .containsExactly(RUN10_ID, RUN9_ID); + + // Commit description and hash are ignored + + assertThat(access.searchRuns(100, null, "d9").stream().map(SearchRunDescription::getId)) + .containsExactly(RUN9_ID); + + assertThat(access.searchRuns(100, null, COMMIT1_HASH.getHash())).isEmpty(); + + // Limit is respected + + assertThat(access.searchRuns(4, null, "a").stream().map(SearchRunDescription::getId)) + .containsExactly(RUN7_ID, RUN2_ID, RUN8_ID, RUN4_ID); + } + + @Test + void getShortRunDescription() { + assertThat(access.getShortRunDescription(RUN1_ID)) + .isEqualTo(new ShortRunDescription(RUN1_ID, null, null, "td1")); + assertThat(access.getShortRunDescription(RUN2_ID)) + .isEqualTo(new ShortRunDescription(RUN2_ID, null, null, "td2")); + assertThat(access.getShortRunDescription(RUN3_ID)) + .isEqualTo(new ShortRunDescription(RUN3_ID, null, null, "td3")); + assertThat(access.getShortRunDescription(RUN4_ID)) + .isEqualTo(new ShortRunDescription(RUN4_ID, null, null, "td4")); + assertThat(access.getShortRunDescription(RUN5_ID)) + .isEqualTo(new ShortRunDescription(RUN5_ID, COMMIT1_HASH.getHash(), "blad9bla", null)); + assertThat(access.getShortRunDescription(RUN6_ID)) + .isEqualTo(new ShortRunDescription(RUN6_ID, COMMIT1_HASH.getHash(), "blad9bla", null)); + assertThat(access.getShortRunDescription(RUN7_ID)) + .isEqualTo(new ShortRunDescription(RUN7_ID, COMMIT1_HASH.getHash(), "blad9bla", null)); + assertThat(access.getShortRunDescription(RUN8_ID)) + .isEqualTo(new ShortRunDescription(RUN8_ID, COMMIT2_HASH.getHash(), "m2", null)); + assertThat(access.getShortRunDescription(RUN9_ID)) + .isEqualTo(new ShortRunDescription(RUN9_ID, null, null, "td9")); + assertThat(access.getShortRunDescription(RUN10_ID)) + .isEqualTo(new ShortRunDescription(RUN10_ID, COMMIT4_HASH.getHash(), "m4", null)); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccessTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccessTest.java index f58a0f45c..864c74830 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccessTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/benchmarkaccess/BenchmarkWriteAccessTest.java @@ -41,234 +41,295 @@ class BenchmarkWriteAccessTest { - private static final RepoId REPO_ID = new RepoId(); - private static final CommitHash COMMIT_HASH = - new CommitHash("4fe01fb5246edddd5b1454b96ecf597bad006666"); - private static final Dimension DIM1 = new Dimension("test", "ing"); - private static final Dimension DIM2 = new Dimension("hello", "world"); - - private DatabaseStorage databaseStorage; - private AvailableDimensionsCache availableDimensionsCache; - private LatestRunCache latestRunCache; - private BenchmarkWriteAccess access; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - TestDb testDb = new TestDb(tempDir); - - testDb.addRepo(REPO_ID); - testDb.addCommit(REPO_ID, COMMIT_HASH); - testDb.addDimension(DIM1); - testDb.addDimension(DIM2); - - databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - availableDimensionsCache = mock(AvailableDimensionsCache.class); - latestRunCache = mock(LatestRunCache.class); - access = new BenchmarkWriteAccess(databaseStorage, availableDimensionsCache, latestRunCache); - } - - @Test - void insertTarRun() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofRight(new TarSource("description", null)), - new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); - - access.insertRun(newRun); - - Run run = access.getRun(runId); - assertThat(run.getId()).isEqualTo(runId); - assertThat(run.getAuthor()).isEqualTo("author"); - assertThat(run.getRunnerName()).isEqualTo("runnerName"); - assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); - assertThat(run.getStartTime()).isEqualTo(startTime); - assertThat(run.getStopTime()).isEqualTo(stopTime); - assertThat(run.getSource()).isEqualTo(Either.ofRight(new TarSource("description", null))); - assertThat(run.getRepoId()).isEqualTo(Optional.empty()); - assertThat(run.getResult()) - .isEqualTo(Either.ofLeft(new RunError("errorMessage", RunErrorType.VELCOM_ERROR))); - } - - @Test - void insertTarRunAttachedToRepo() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofRight(new TarSource("description", REPO_ID)), - new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); - - access.insertRun(newRun); - - Run run = access.getRun(runId); - assertThat(run.getId()).isEqualTo(runId); - assertThat(run.getAuthor()).isEqualTo("author"); - assertThat(run.getRunnerName()).isEqualTo("runnerName"); - assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); - assertThat(run.getStartTime()).isEqualTo(startTime); - assertThat(run.getStopTime()).isEqualTo(stopTime); - assertThat(run.getSource()).isEqualTo(Either.ofRight(new TarSource("description", REPO_ID))); - assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); - assertThat(run.getResult()) - .isEqualTo(Either.ofLeft(new RunError("errorMessage", RunErrorType.VELCOM_ERROR))); - } - - @Test - void insertCommitRun() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), - new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); - - access.insertRun(newRun); - - Run run = access.getRun(runId); - assertThat(run.getId()).isEqualTo(runId); - assertThat(run.getAuthor()).isEqualTo("author"); - assertThat(run.getRunnerName()).isEqualTo("runnerName"); - assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); - assertThat(run.getStartTime()).isEqualTo(startTime); - assertThat(run.getStopTime()).isEqualTo(stopTime); - assertThat(run.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH))); - assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); - assertThat(run.getResult()) - .isEqualTo(Either.ofLeft(new RunError("errorMessage", RunErrorType.VELCOM_ERROR))); - } - - @Test - void insertSuccessfulRun() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), List.of( - new NewMeasurement(runId, DIM1, null, null, new MeasurementValues(List.of(1d, 2d, 3d))), - new NewMeasurement(runId, DIM2, null, null, new MeasurementValues(List.of(4d))) - )); - - access.insertRun(newRun); - - Run run = access.getRun(runId); - assertThat(run.getId()).isEqualTo(runId); - assertThat(run.getAuthor()).isEqualTo("author"); - assertThat(run.getRunnerName()).isEqualTo("runnerName"); - assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); - assertThat(run.getStartTime()).isEqualTo(startTime); - assertThat(run.getStopTime()).isEqualTo(stopTime); - assertThat(run.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH))); - assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); - assertThat(run.getResult().getRight()).isPresent(); - assertThat(run.getResult().getRight().get()).containsExactlyInAnyOrder( - new Measurement(runId, DIM1, Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d)))), - new Measurement(runId, DIM2, Either.ofRight(new MeasurementValues(List.of(4d)))) - ); - } - - @Test - void insertPartlySuccessfulRun() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), List.of( - new NewMeasurement(runId, DIM1, null, null, new MeasurementValues(List.of(1d, 2d, 3d))), - new NewMeasurement(runId, DIM2, null, null, new MeasurementError("errorMessage")) - )); - - access.insertRun(newRun); - - Run run = access.getRun(runId); - assertThat(run.getId()).isEqualTo(runId); - assertThat(run.getAuthor()).isEqualTo("author"); - assertThat(run.getRunnerName()).isEqualTo("runnerName"); - assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); - assertThat(run.getStartTime()).isEqualTo(startTime); - assertThat(run.getStopTime()).isEqualTo(stopTime); - assertThat(run.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH))); - assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); - assertThat(run.getResult().getRight()).isPresent(); - assertThat(run.getResult().getRight().get()).containsExactlyInAnyOrder( - new Measurement(runId, DIM1, Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d)))), - new Measurement(runId, DIM2, Either.ofLeft(new MeasurementError("errorMessage"))) - ); - } - - @Test - void insertingRunUpdatesDimensions() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - Dimension dim3 = new Dimension("a", "b"); - Dimension dim4 = new Dimension("c", "d"); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), List.of( - new NewMeasurement(runId, DIM1, new Unit("asdf"), null, - new MeasurementValues(List.of(1d, 2d, 3d))), - new NewMeasurement(runId, DIM2, null, Interpretation.MORE_IS_BETTER, - new MeasurementError("errorMessage")), - new NewMeasurement(runId, dim3, null, null, new MeasurementValues(List.of(4d))), - new NewMeasurement(runId, dim4, new Unit("foo"), Interpretation.LESS_IS_BETTER, - new MeasurementValues(List.of(5d))) - )); - - access.insertRun(newRun); - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - Map dimMap = db.dsl() - .selectFrom(DIMENSION) - .stream() - .collect(toMap( - it -> new Dimension(it.getBenchmark(), it.getMetric()), - it -> it - )); - - assertThat(dimMap).containsOnlyKeys(DIM1, DIM2, dim3, dim4); - - DimensionRecord dim1Record = dimMap.get(DIM1); - assertThat(dim1Record.getUnit()).isEqualTo(new Unit("asdf").getName()); - assertThat(dim1Record.getInterpretation()) - .isEqualTo(Interpretation.DEFAULT.getTextualRepresentation()); - - DimensionRecord dim2Record = dimMap.get(DIM2); - assertThat(dim2Record.getUnit()).isEqualTo(Unit.DEFAULT.getName()); - assertThat(dim2Record.getInterpretation()) - .isEqualTo(Interpretation.MORE_IS_BETTER.getTextualRepresentation()); - - DimensionRecord dim3Record = dimMap.get(dim3); - assertThat(dim3Record.getUnit()).isEqualTo(Unit.DEFAULT.getName()); - assertThat(dim3Record.getInterpretation()) - .isEqualTo(Interpretation.DEFAULT.getTextualRepresentation()); - - DimensionRecord dim4Record = dimMap.get(dim4); - assertThat(dim4Record.getUnit()).isEqualTo(new Unit("foo").getName()); - assertThat(dim4Record.getInterpretation()) - .isEqualTo(Interpretation.LESS_IS_BETTER.getTextualRepresentation()); - } - } - - @Test - void insertingRunInvalidatesCaches() { - RunId runId = new RunId(); - Instant startTime = Instant.ofEpochSecond(1600010001); - Instant stopTime = Instant.ofEpochSecond(1600010006); - - NewRun newRun = new NewRun(runId, "author", "runnerName", "runnerInfo", startTime, stopTime, - Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), - new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); - - access.insertRun(newRun); - - verify(availableDimensionsCache, atLeastOnce()).invalidate(REPO_ID); - verify(latestRunCache, atLeastOnce()).invalidate(REPO_ID, COMMIT_HASH); - } + private static final RepoId REPO_ID = new RepoId(); + private static final CommitHash COMMIT_HASH = + new CommitHash("4fe01fb5246edddd5b1454b96ecf597bad006666"); + private static final Dimension DIM1 = new Dimension("test", "ing"); + private static final Dimension DIM2 = new Dimension("hello", "world"); + + private DatabaseStorage databaseStorage; + private AvailableDimensionsCache availableDimensionsCache; + private LatestRunCache latestRunCache; + private BenchmarkWriteAccess access; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + TestDb testDb = new TestDb(tempDir); + + testDb.addRepo(REPO_ID); + testDb.addCommit(REPO_ID, COMMIT_HASH); + testDb.addDimension(DIM1); + testDb.addDimension(DIM2); + + databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + availableDimensionsCache = mock(AvailableDimensionsCache.class); + latestRunCache = mock(LatestRunCache.class); + access = new BenchmarkWriteAccess(databaseStorage, availableDimensionsCache, latestRunCache); + } + + @Test + void insertTarRun() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofRight(new TarSource("description", null)), + new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); + + access.insertRun(newRun); + + Run run = access.getRun(runId); + assertThat(run.getId()).isEqualTo(runId); + assertThat(run.getAuthor()).isEqualTo("author"); + assertThat(run.getRunnerName()).isEqualTo("runnerName"); + assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); + assertThat(run.getStartTime()).isEqualTo(startTime); + assertThat(run.getStopTime()).isEqualTo(stopTime); + assertThat(run.getSource()).isEqualTo(Either.ofRight(new TarSource("description", null))); + assertThat(run.getRepoId()).isEqualTo(Optional.empty()); + assertThat(run.getResult()) + .isEqualTo(Either.ofLeft(new RunError("errorMessage", RunErrorType.VELCOM_ERROR))); + } + + @Test + void insertTarRunAttachedToRepo() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofRight(new TarSource("description", REPO_ID)), + new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); + + access.insertRun(newRun); + + Run run = access.getRun(runId); + assertThat(run.getId()).isEqualTo(runId); + assertThat(run.getAuthor()).isEqualTo("author"); + assertThat(run.getRunnerName()).isEqualTo("runnerName"); + assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); + assertThat(run.getStartTime()).isEqualTo(startTime); + assertThat(run.getStopTime()).isEqualTo(stopTime); + assertThat(run.getSource()).isEqualTo(Either.ofRight(new TarSource("description", REPO_ID))); + assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); + assertThat(run.getResult()) + .isEqualTo(Either.ofLeft(new RunError("errorMessage", RunErrorType.VELCOM_ERROR))); + } + + @Test + void insertCommitRun() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), + new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); + + access.insertRun(newRun); + + Run run = access.getRun(runId); + assertThat(run.getId()).isEqualTo(runId); + assertThat(run.getAuthor()).isEqualTo("author"); + assertThat(run.getRunnerName()).isEqualTo("runnerName"); + assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); + assertThat(run.getStartTime()).isEqualTo(startTime); + assertThat(run.getStopTime()).isEqualTo(stopTime); + assertThat(run.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH))); + assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); + assertThat(run.getResult()) + .isEqualTo(Either.ofLeft(new RunError("errorMessage", RunErrorType.VELCOM_ERROR))); + } + + @Test + void insertSuccessfulRun() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), + List.of( + new NewMeasurement( + runId, DIM1, null, null, new MeasurementValues(List.of(1d, 2d, 3d))), + new NewMeasurement(runId, DIM2, null, null, new MeasurementValues(List.of(4d))))); + + access.insertRun(newRun); + + Run run = access.getRun(runId); + assertThat(run.getId()).isEqualTo(runId); + assertThat(run.getAuthor()).isEqualTo("author"); + assertThat(run.getRunnerName()).isEqualTo("runnerName"); + assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); + assertThat(run.getStartTime()).isEqualTo(startTime); + assertThat(run.getStopTime()).isEqualTo(stopTime); + assertThat(run.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH))); + assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); + assertThat(run.getResult().getRight()).isPresent(); + assertThat(run.getResult().getRight().get()) + .containsExactlyInAnyOrder( + new Measurement( + runId, DIM1, Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d)))), + new Measurement(runId, DIM2, Either.ofRight(new MeasurementValues(List.of(4d))))); + } + + @Test + void insertPartlySuccessfulRun() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), + List.of( + new NewMeasurement( + runId, DIM1, null, null, new MeasurementValues(List.of(1d, 2d, 3d))), + new NewMeasurement(runId, DIM2, null, null, new MeasurementError("errorMessage")))); + + access.insertRun(newRun); + + Run run = access.getRun(runId); + assertThat(run.getId()).isEqualTo(runId); + assertThat(run.getAuthor()).isEqualTo("author"); + assertThat(run.getRunnerName()).isEqualTo("runnerName"); + assertThat(run.getRunnerInfo()).isEqualTo("runnerInfo"); + assertThat(run.getStartTime()).isEqualTo(startTime); + assertThat(run.getStopTime()).isEqualTo(stopTime); + assertThat(run.getSource()).isEqualTo(Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH))); + assertThat(run.getRepoId()).isEqualTo(Optional.of(REPO_ID)); + assertThat(run.getResult().getRight()).isPresent(); + assertThat(run.getResult().getRight().get()) + .containsExactlyInAnyOrder( + new Measurement( + runId, DIM1, Either.ofRight(new MeasurementValues(List.of(1d, 2d, 3d)))), + new Measurement(runId, DIM2, Either.ofLeft(new MeasurementError("errorMessage")))); + } + + @Test + void insertingRunUpdatesDimensions() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + Dimension dim3 = new Dimension("a", "b"); + Dimension dim4 = new Dimension("c", "d"); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), + List.of( + new NewMeasurement( + runId, + DIM1, + new Unit("asdf"), + null, + new MeasurementValues(List.of(1d, 2d, 3d))), + new NewMeasurement( + runId, + DIM2, + null, + Interpretation.MORE_IS_BETTER, + new MeasurementError("errorMessage")), + new NewMeasurement(runId, dim3, null, null, new MeasurementValues(List.of(4d))), + new NewMeasurement( + runId, + dim4, + new Unit("foo"), + Interpretation.LESS_IS_BETTER, + new MeasurementValues(List.of(5d))))); + + access.insertRun(newRun); + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + Map dimMap = + db.dsl().selectFrom(DIMENSION).stream() + .collect(toMap(it -> new Dimension(it.getBenchmark(), it.getMetric()), it -> it)); + + assertThat(dimMap).containsOnlyKeys(DIM1, DIM2, dim3, dim4); + + DimensionRecord dim1Record = dimMap.get(DIM1); + assertThat(dim1Record.getUnit()).isEqualTo(new Unit("asdf").getName()); + assertThat(dim1Record.getInterpretation()) + .isEqualTo(Interpretation.DEFAULT.getTextualRepresentation()); + + DimensionRecord dim2Record = dimMap.get(DIM2); + assertThat(dim2Record.getUnit()).isEqualTo(Unit.DEFAULT.getName()); + assertThat(dim2Record.getInterpretation()) + .isEqualTo(Interpretation.MORE_IS_BETTER.getTextualRepresentation()); + + DimensionRecord dim3Record = dimMap.get(dim3); + assertThat(dim3Record.getUnit()).isEqualTo(Unit.DEFAULT.getName()); + assertThat(dim3Record.getInterpretation()) + .isEqualTo(Interpretation.DEFAULT.getTextualRepresentation()); + + DimensionRecord dim4Record = dimMap.get(dim4); + assertThat(dim4Record.getUnit()).isEqualTo(new Unit("foo").getName()); + assertThat(dim4Record.getInterpretation()) + .isEqualTo(Interpretation.LESS_IS_BETTER.getTextualRepresentation()); + } + } + + @Test + void insertingRunInvalidatesCaches() { + RunId runId = new RunId(); + Instant startTime = Instant.ofEpochSecond(1600010001); + Instant stopTime = Instant.ofEpochSecond(1600010006); + + NewRun newRun = + new NewRun( + runId, + "author", + "runnerName", + "runnerInfo", + startTime, + stopTime, + Either.ofLeft(new CommitSource(REPO_ID, COMMIT_HASH)), + new RunError("errorMessage", RunErrorType.VELCOM_ERROR)); + + access.insertRun(newRun); + + verify(availableDimensionsCache, atLeastOnce()).invalidate(REPO_ID); + verify(latestRunCache, atLeastOnce()).invalidate(REPO_ID, COMMIT_HASH); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccessTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccessTest.java index b73286d13..e10a4b14f 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccessTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/CommitReadAccessTest.java @@ -18,497 +18,913 @@ import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; class CommitReadAccessTest { - /* - * The commits for this test have the following structure. - * - * H - * / - * C -- D -- E <= T - * / \ - * A -- B ------ F -- G <= U - * \ - * I - * - * Commit E is on a tracked branch named T while commit G is on an untracked branch named U. - * Thus, commits A, B, C, D and E are tracked while commits F and G are untracked. - * Commits H and I are not reachable from any branch. - */ - - private static final RepoId REPO_ID = new RepoId(); - - private static final CommitHash COMM_A_HASH = - new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"); - private static final CommitHash COMM_B_HASH = - new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"); - private static final CommitHash COMM_C_HASH = - new CommitHash("a23577b29bfe6e384bba835b4453d0fc7f33855c"); - private static final CommitHash COMM_D_HASH = - new CommitHash("2acba5b560711dc4c8e53c356238862c07712eca"); - private static final CommitHash COMM_E_HASH = - new CommitHash("5dd4f1f6f0b3d5d5d830c6e4789d6f161496fa81"); - private static final CommitHash COMM_F_HASH = - new CommitHash("50061f6cb32961b06db0ade9d5114dc1ec3690bd"); - private static final CommitHash COMM_G_HASH = - new CommitHash("8ae1895217862be76b98bc46fb94cea9e006e83a"); - private static final CommitHash COMM_H_HASH = - new CommitHash("c7eed2922f1a406edec6c7e043309a92e6a16a89"); - private static final CommitHash COMM_I_HASH = - new CommitHash("e9a98785fabfa10307ee4ab637537a67c86f0e67"); - - private static final BranchName BRANCH_T = BranchName.fromName("T"); - private static final BranchName BRANCH_U = BranchName.fromName("U"); - - private CommitReadAccess access; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - TestDb testDb = new TestDb(tempDir); - - testDb.addRepo(REPO_ID); - testDb.addCommit(REPO_ID, COMM_A_HASH, true, true, true, - "aA", Instant.ofEpochSecond(1600010001), "cA", Instant.ofEpochSecond(1600010002), "A"); - testDb.addCommit(REPO_ID, COMM_B_HASH, true, true, true, - "aB", Instant.ofEpochSecond(1600020001), "cB", Instant.ofEpochSecond(1600020002), "B"); - testDb.addCommit(REPO_ID, COMM_C_HASH, true, true, true, - "aC", Instant.ofEpochSecond(1600030001), "cC", Instant.ofEpochSecond(1600030002), "C"); - testDb.addCommit(REPO_ID, COMM_D_HASH, true, true, true, - "aD", Instant.ofEpochSecond(1600040001), "cD", Instant.ofEpochSecond(1600040002), "D"); - testDb.addCommit(REPO_ID, COMM_E_HASH, true, true, true, - "aE", Instant.ofEpochSecond(1600050001), "cE", Instant.ofEpochSecond(1600050002), "E"); - testDb.addCommit(REPO_ID, COMM_F_HASH, true, false, true, - "aF", Instant.ofEpochSecond(1600060001), "cF", Instant.ofEpochSecond(1600060002), "F"); - testDb.addCommit(REPO_ID, COMM_G_HASH, true, false, true, - "aG", Instant.ofEpochSecond(1600070001), "cG", Instant.ofEpochSecond(1600070002), "G"); - testDb.addCommit(REPO_ID, COMM_H_HASH, false, false, true, - "aH", Instant.ofEpochSecond(1600080001), "cH", Instant.ofEpochSecond(1600080002), "H"); - testDb.addCommit(REPO_ID, COMM_I_HASH, false, false, true, - "aI", Instant.ofEpochSecond(1600090001), "cI", Instant.ofEpochSecond(1600090002), "I"); - testDb.addCommitRel(REPO_ID, COMM_A_HASH, COMM_B_HASH); - testDb.addCommitRel(REPO_ID, COMM_B_HASH, COMM_C_HASH); - testDb.addCommitRel(REPO_ID, COMM_B_HASH, COMM_F_HASH); - testDb.addCommitRel(REPO_ID, COMM_C_HASH, COMM_D_HASH); - testDb.addCommitRel(REPO_ID, COMM_D_HASH, COMM_E_HASH); - testDb.addCommitRel(REPO_ID, COMM_D_HASH, COMM_F_HASH); - testDb.addCommitRel(REPO_ID, COMM_D_HASH, COMM_H_HASH); - testDb.addCommitRel(REPO_ID, COMM_F_HASH, COMM_G_HASH); - testDb.addCommitRel(REPO_ID, COMM_F_HASH, COMM_I_HASH); - testDb.addBranch(REPO_ID, BRANCH_T, COMM_E_HASH, true); - testDb.addBranch(REPO_ID, BRANCH_U, COMM_G_HASH, false); - - DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - access = new CommitReadAccess(databaseStorage); - } - - private void checkCommit(Commit commit, RepoId repoId, CommitHash commitHash, boolean isReachable, - boolean isTracked, String author, int authorDate, String committer, int committerDate, - String message) { - - assertThat(commit.getRepoId()).isEqualTo(repoId); - assertThat(commit.getHash()).isEqualTo(commitHash); - assertThat(commit.isReachable()).isEqualTo(isReachable); - assertThat(commit.isTracked()).isEqualTo(isTracked); - assertThat(commit.getAuthor()).isEqualTo(author); - assertThat(commit.getAuthorDate()).isEqualTo(Instant.ofEpochSecond(authorDate)); - assertThat(commit.getCommitter()).isEqualTo(committer); - assertThat(commit.getCommitterDate()).isEqualTo(Instant.ofEpochSecond(committerDate)); - assertThat(commit.getMessage()).isEqualTo(message); - } - - private void checkFullCommit(FullCommit commit, RepoId repoId, CommitHash commitHash, - boolean isReachable, boolean isTracked, String author, int authorDate, String committer, - int committerDate, String message, List parents, List children) { - - checkCommit(commit, repoId, commitHash, isReachable, isTracked, author, authorDate, committer, - committerDate, message); - - assertThat(commit.getParentHashes()).containsExactlyInAnyOrderElementsOf(parents); - assertThat(commit.getChildHashes()).containsExactlyInAnyOrderElementsOf(children); - } - - @Test - void getCommit() { - checkCommit(access.getCommit(REPO_ID, COMM_A_HASH), REPO_ID, COMM_A_HASH, true, true, - "aA", 1600010001, "cA", 1600010002, "A"); - checkCommit(access.getCommit(REPO_ID, COMM_B_HASH), REPO_ID, COMM_B_HASH, true, true, - "aB", 1600020001, "cB", 1600020002, "B"); - checkCommit(access.getCommit(REPO_ID, COMM_C_HASH), REPO_ID, COMM_C_HASH, true, true, - "aC", 1600030001, "cC", 1600030002, "C"); - checkCommit(access.getCommit(REPO_ID, COMM_D_HASH), REPO_ID, COMM_D_HASH, true, true, - "aD", 1600040001, "cD", 1600040002, "D"); - checkCommit(access.getCommit(REPO_ID, COMM_E_HASH), REPO_ID, COMM_E_HASH, true, true, - "aE", 1600050001, "cE", 1600050002, "E"); - checkCommit(access.getCommit(REPO_ID, COMM_F_HASH), REPO_ID, COMM_F_HASH, true, false, - "aF", 1600060001, "cF", 1600060002, "F"); - checkCommit(access.getCommit(REPO_ID, COMM_G_HASH), REPO_ID, COMM_G_HASH, true, false, - "aG", 1600070001, "cG", 1600070002, "G"); - checkCommit(access.getCommit(REPO_ID, COMM_H_HASH), REPO_ID, COMM_H_HASH, false, false, - "aH", 1600080001, "cH", 1600080002, "H"); - checkCommit(access.getCommit(REPO_ID, COMM_I_HASH), REPO_ID, COMM_I_HASH, false, false, - "aI", 1600090001, "cI", 1600090002, "I"); - - RepoId nonexistentId = new RepoId(); - CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); - - assertThatThrownBy(() -> access.getCommit(REPO_ID, nonexistentHash)) - .isInstanceOf(NoSuchCommitException.class) - .satisfies(throwable -> { - NoSuchCommitException e = (NoSuchCommitException) throwable; - assertThat(e.getRepoId()).isEqualTo(REPO_ID); - assertThat(e.getCommitHash()).isEqualTo(nonexistentHash); - }); - - assertThatThrownBy(() -> access.getCommit(nonexistentId, COMM_A_HASH)) - .isInstanceOf(NoSuchCommitException.class) - .satisfies(throwable -> { - NoSuchCommitException e = (NoSuchCommitException) throwable; - assertThat(e.getRepoId()).isEqualTo(nonexistentId); - assertThat(e.getCommitHash()).isEqualTo(COMM_A_HASH); - }); - } - - @Test - void guarding() { - RepoId nonexistentId = new RepoId(); - CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); - - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_A_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_B_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_C_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_D_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_E_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_F_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_G_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_H_HASH)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_I_HASH)).doesNotThrowAnyException(); - - assertThatThrownBy(() -> access.guardCommitExists(REPO_ID, nonexistentHash)) - .isInstanceOf(NoSuchCommitException.class) - .satisfies(throwable -> { - NoSuchCommitException e = (NoSuchCommitException) throwable; - assertThat(e.getRepoId()).isEqualTo(REPO_ID); - assertThat(e.getCommitHash()).isEqualTo(nonexistentHash); - }); - - assertThatThrownBy(() -> access.guardCommitExists(nonexistentId, COMM_A_HASH)) - .isInstanceOf(NoSuchCommitException.class) - .satisfies(throwable -> { - NoSuchCommitException e = (NoSuchCommitException) throwable; - assertThat(e.getRepoId()).isEqualTo(nonexistentId); - assertThat(e.getCommitHash()).isEqualTo(COMM_A_HASH); - }); - } - - @Test - void getCommits() { - RepoId nonexistentId = new RepoId(); - CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); - - List commits = access - .getCommits(REPO_ID, List.of(COMM_B_HASH, COMM_G_HASH, COMM_I_HASH, nonexistentHash)); - - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_G_HASH, COMM_I_HASH); - - assertThat(commits.stream() - .map(Commit::getRepoId) - .collect(toSet())) - .containsExactlyInAnyOrder(REPO_ID); - - Map commitMap = commits.stream() - .collect(toMap(Commit::getHash, it -> it)); - - checkCommit(commitMap.get(COMM_B_HASH), REPO_ID, COMM_B_HASH, true, true, - "aB", 1600020001, "cB", 1600020002, "B"); - checkCommit(commitMap.get(COMM_G_HASH), REPO_ID, COMM_G_HASH, true, false, - "aG", 1600070001, "cG", 1600070002, "G"); - checkCommit(commitMap.get(COMM_I_HASH), REPO_ID, COMM_I_HASH, false, false, - "aI", 1600090001, "cI", 1600090002, "I"); - - assertThat(access.getCommits(nonexistentId, List.of(COMM_A_HASH, COMM_E_HASH, COMM_I_HASH))) - .isEmpty(); - } - - @Test - void getFullCommit() { - checkFullCommit(access.getFullCommit(REPO_ID, COMM_A_HASH), REPO_ID, COMM_A_HASH, true, true, - "aA", 1600010001, "cA", 1600010002, "A", - List.of(), List.of(COMM_B_HASH)); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_B_HASH), REPO_ID, COMM_B_HASH, true, true, - "aB", 1600020001, "cB", 1600020002, "B", - List.of(COMM_A_HASH), List.of(COMM_C_HASH, COMM_F_HASH)); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_C_HASH), REPO_ID, COMM_C_HASH, true, true, - "aC", 1600030001, "cC", 1600030002, "C", - List.of(COMM_B_HASH), List.of(COMM_D_HASH)); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_D_HASH), REPO_ID, COMM_D_HASH, true, true, - "aD", 1600040001, "cD", 1600040002, "D", - List.of(COMM_C_HASH), List.of(COMM_E_HASH, COMM_F_HASH, COMM_H_HASH)); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_E_HASH), REPO_ID, COMM_E_HASH, true, true, - "aE", 1600050001, "cE", 1600050002, "E", - List.of(COMM_D_HASH), List.of()); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_F_HASH), REPO_ID, COMM_F_HASH, true, false, - "aF", 1600060001, "cF", 1600060002, "F", - List.of(COMM_B_HASH, COMM_D_HASH), List.of(COMM_G_HASH, COMM_I_HASH)); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_G_HASH), REPO_ID, COMM_G_HASH, true, false, - "aG", 1600070001, "cG", 1600070002, "G", - List.of(COMM_F_HASH), List.of()); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_H_HASH), REPO_ID, COMM_H_HASH, false, false, - "aH", 1600080001, "cH", 1600080002, "H", - List.of(COMM_D_HASH), List.of()); - checkFullCommit(access.getFullCommit(REPO_ID, COMM_I_HASH), REPO_ID, COMM_I_HASH, false, false, - "aI", 1600090001, "cI", 1600090002, "I", - List.of(COMM_F_HASH), List.of()); - - RepoId nonexistentId = new RepoId(); - CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); - - assertThatThrownBy(() -> access.getFullCommit(REPO_ID, nonexistentHash)) - .isInstanceOf(NoSuchCommitException.class) - .satisfies(throwable -> { - NoSuchCommitException e = (NoSuchCommitException) throwable; - assertThat(e.getRepoId()).isEqualTo(REPO_ID); - assertThat(e.getCommitHash()).isEqualTo(nonexistentHash); - }); - - assertThatThrownBy(() -> access.getFullCommit(nonexistentId, COMM_A_HASH)) - .isInstanceOf(NoSuchCommitException.class) - .satisfies(throwable -> { - NoSuchCommitException e = (NoSuchCommitException) throwable; - assertThat(e.getRepoId()).isEqualTo(nonexistentId); - assertThat(e.getCommitHash()).isEqualTo(COMM_A_HASH); - }); - } - - @Test - void promoteCommits() { - CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); - - // Missing COMM_I_HASH and including one nonexistent hash - List commits = access.promoteCommits(access.getCommits(REPO_ID, - List.of(COMM_A_HASH, COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH, COMM_F_HASH, - COMM_G_HASH, COMM_H_HASH, nonexistentHash))); - - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_A_HASH, COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH, - COMM_F_HASH, COMM_G_HASH, COMM_H_HASH); - - Map commitMap = commits.stream() - .collect(toMap(Commit::getHash, it -> it)); - - checkFullCommit(commitMap.get(COMM_A_HASH), REPO_ID, COMM_A_HASH, true, true, - "aA", 1600010001, "cA", 1600010002, "A", - List.of(), List.of(COMM_B_HASH)); - checkFullCommit(commitMap.get(COMM_B_HASH), REPO_ID, COMM_B_HASH, true, true, - "aB", 1600020001, "cB", 1600020002, "B", - List.of(COMM_A_HASH), List.of(COMM_C_HASH, COMM_F_HASH)); - checkFullCommit(commitMap.get(COMM_C_HASH), REPO_ID, COMM_C_HASH, true, true, - "aC", 1600030001, "cC", 1600030002, "C", - List.of(COMM_B_HASH), List.of(COMM_D_HASH)); - checkFullCommit(commitMap.get(COMM_D_HASH), REPO_ID, COMM_D_HASH, true, true, - "aD", 1600040001, "cD", 1600040002, "D", - List.of(COMM_C_HASH), List.of(COMM_E_HASH, COMM_F_HASH, COMM_H_HASH)); - checkFullCommit(commitMap.get(COMM_E_HASH), REPO_ID, COMM_E_HASH, true, true, - "aE", 1600050001, "cE", 1600050002, "E", - List.of(COMM_D_HASH), List.of()); - checkFullCommit(commitMap.get(COMM_F_HASH), REPO_ID, COMM_F_HASH, true, false, - "aF", 1600060001, "cF", 1600060002, "F", - List.of(COMM_B_HASH, COMM_D_HASH), List.of(COMM_G_HASH, COMM_I_HASH)); - checkFullCommit(commitMap.get(COMM_G_HASH), REPO_ID, COMM_G_HASH, true, false, - "aG", 1600070001, "cG", 1600070002, "G", - List.of(COMM_F_HASH), List.of()); - checkFullCommit(commitMap.get(COMM_H_HASH), REPO_ID, COMM_H_HASH, false, false, - "aH", 1600080001, "cH", 1600080002, "H", - List.of(COMM_D_HASH), List.of()); - } - - @Test - void getParentsAndChildren() { - assertThat(access.getParentHashes(REPO_ID, COMM_A_HASH)).isEmpty(); - assertThat(access.getParentHashes(REPO_ID, COMM_B_HASH)).containsExactlyInAnyOrder(COMM_A_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_C_HASH)).containsExactlyInAnyOrder(COMM_B_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_D_HASH)).containsExactlyInAnyOrder(COMM_C_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_E_HASH)).containsExactlyInAnyOrder(COMM_D_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_F_HASH)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_G_HASH)).containsExactlyInAnyOrder(COMM_F_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_H_HASH)).containsExactlyInAnyOrder(COMM_D_HASH); - assertThat(access.getParentHashes(REPO_ID, COMM_I_HASH)).containsExactlyInAnyOrder(COMM_F_HASH); - - assertThat(access.getChildHashes(REPO_ID, COMM_A_HASH)).containsExactlyInAnyOrder(COMM_B_HASH); - assertThat(access.getChildHashes(REPO_ID, COMM_B_HASH)) - .containsExactlyInAnyOrder(COMM_C_HASH, COMM_F_HASH); - assertThat(access.getChildHashes(REPO_ID, COMM_C_HASH)).containsExactlyInAnyOrder(COMM_D_HASH); - assertThat(access.getChildHashes(REPO_ID, COMM_D_HASH)) - .containsExactlyInAnyOrder(COMM_E_HASH, COMM_F_HASH, COMM_H_HASH); - assertThat(access.getChildHashes(REPO_ID, COMM_E_HASH)).isEmpty(); - assertThat(access.getChildHashes(REPO_ID, COMM_F_HASH)) - .containsExactlyInAnyOrder(COMM_G_HASH, COMM_I_HASH); - assertThat(access.getChildHashes(REPO_ID, COMM_G_HASH)).isEmpty(); - assertThat(access.getChildHashes(REPO_ID, COMM_H_HASH)).isEmpty(); - assertThat(access.getChildHashes(REPO_ID, COMM_I_HASH)).isEmpty(); - } - - @Test - void getDescendantCommits() { - assertThat(access.getDescendantCommits(REPO_ID, COMM_A_HASH)) - .containsExactlyInAnyOrder(COMM_A_HASH, COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); - assertThat(access.getDescendantCommits(REPO_ID, COMM_B_HASH)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); - assertThat(access.getDescendantCommits(REPO_ID, COMM_C_HASH)) - .containsExactlyInAnyOrder(COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); - assertThat(access.getDescendantCommits(REPO_ID, COMM_D_HASH)) - .containsExactlyInAnyOrder(COMM_D_HASH, COMM_E_HASH); - assertThat(access.getDescendantCommits(REPO_ID, COMM_E_HASH)) - .containsExactlyInAnyOrder(COMM_E_HASH); - assertThat(access.getDescendantCommits(REPO_ID, COMM_F_HASH)).isEmpty(); - assertThat(access.getDescendantCommits(REPO_ID, COMM_G_HASH)).isEmpty(); - assertThat(access.getDescendantCommits(REPO_ID, COMM_H_HASH)).isEmpty(); - assertThat(access.getDescendantCommits(REPO_ID, COMM_I_HASH)).isEmpty(); - - RepoId nonexistentId = new RepoId(); - CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); - - assertThat(access.getDescendantCommits(nonexistentId, COMM_B_HASH)).isEmpty(); - assertThat(access.getDescendantCommits(REPO_ID, nonexistentHash)).isEmpty(); - } - - @Test - void getTrackedCommitsBetween() { - // Check that untracked and unreachable commits are not included - - List commits = access.getTrackedCommitsBetween(REPO_ID, - Instant.ofEpochSecond(1600030000), Instant.ofEpochSecond(1600080005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); - - // Check off-by-one errors and that committer and not author time is used - - commits = access.getTrackedCommitsBetween(REPO_ID, - Instant.ofEpochSecond(1600040003), Instant.ofEpochSecond(1600050002)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_E_HASH); - - commits = access.getTrackedCommitsBetween(REPO_ID, - Instant.ofEpochSecond(1600040002), Instant.ofEpochSecond(1600050001)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_D_HASH); - } - - @Test - void getFirstParentOfBranch() { - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_A_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_B_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_C_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_D_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_E_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_F_HASH)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_G_HASH)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_H_HASH)) - .containsExactlyInAnyOrder(COMM_D_HASH); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_I_HASH)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); - - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_A_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_B_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_C_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_D_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_E_HASH)) - .containsExactlyInAnyOrder(COMM_D_HASH); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_F_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_G_HASH)) - .isEmpty(); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_H_HASH)) - .containsExactlyInAnyOrder(COMM_D_HASH); - assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_I_HASH)) - .containsExactlyInAnyOrder(COMM_F_HASH); - } - - @Test - void getCommitsBetween() { - // From an untracked branch - - List commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("U")), - Instant.ofEpochSecond(1600020000), Instant.ofEpochSecond(1600060005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_F_HASH); - - commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("U")), - Instant.ofEpochSecond(1600020000), Instant.ofEpochSecond(1600090005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_F_HASH, COMM_G_HASH); - - // From a tracked branch - - commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("T")), - Instant.ofEpochSecond(1600020000), Instant.ofEpochSecond(1600040005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH); - - commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("T")), - Instant.ofEpochSecond(1600020000), Instant.ofEpochSecond(1600090005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); - - // From both branches - - commits = access.getCommitsBetween(REPO_ID, - List.of(BranchName.fromName("T"), BranchName.fromName("U")), - Instant.ofEpochSecond(1600020000), Instant.ofEpochSecond(1600060005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH, COMM_F_HASH); - - commits = access.getCommitsBetween(REPO_ID, - List.of(BranchName.fromName("T"), BranchName.fromName("U")), - Instant.ofEpochSecond(1600020000), Instant.ofEpochSecond(1600090005)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH, COMM_F_HASH, - COMM_G_HASH); - - // Check off-by-one errors and that committer and not author time is used - - commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("T")), - Instant.ofEpochSecond(1600040002), Instant.ofEpochSecond(1600050001)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_D_HASH); - - commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("T")), - Instant.ofEpochSecond(1600040001), Instant.ofEpochSecond(1600050002)); - assertThat(commits.stream() - .map(Commit::getHash)) - .containsExactlyInAnyOrder(COMM_D_HASH, COMM_E_HASH); - - commits = access.getCommitsBetween(REPO_ID, List.of(BranchName.fromName("T")), - Instant.ofEpochSecond(1600040003), Instant.ofEpochSecond(1600050000)); - assertThat(commits).isEmpty(); - } + /* + * The commits for this test have the following structure. + * + * H + * / + * C -- D -- E <= T + * / \ + * A -- B ------ F -- G <= U + * \ + * I + * + * Commit E is on a tracked branch named T while commit G is on an untracked branch named U. + * Thus, commits A, B, C, D and E are tracked while commits F and G are untracked. + * Commits H and I are not reachable from any branch. + */ + + private static final RepoId REPO_ID = new RepoId(); + + private static final CommitHash COMM_A_HASH = + new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"); + private static final CommitHash COMM_B_HASH = + new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"); + private static final CommitHash COMM_C_HASH = + new CommitHash("a23577b29bfe6e384bba835b4453d0fc7f33855c"); + private static final CommitHash COMM_D_HASH = + new CommitHash("2acba5b560711dc4c8e53c356238862c07712eca"); + private static final CommitHash COMM_E_HASH = + new CommitHash("5dd4f1f6f0b3d5d5d830c6e4789d6f161496fa81"); + private static final CommitHash COMM_F_HASH = + new CommitHash("50061f6cb32961b06db0ade9d5114dc1ec3690bd"); + private static final CommitHash COMM_G_HASH = + new CommitHash("8ae1895217862be76b98bc46fb94cea9e006e83a"); + private static final CommitHash COMM_H_HASH = + new CommitHash("c7eed2922f1a406edec6c7e043309a92e6a16a89"); + private static final CommitHash COMM_I_HASH = + new CommitHash("e9a98785fabfa10307ee4ab637537a67c86f0e67"); + + private static final BranchName BRANCH_T = BranchName.fromName("T"); + private static final BranchName BRANCH_U = BranchName.fromName("U"); + + private CommitReadAccess access; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + TestDb testDb = new TestDb(tempDir); + + testDb.addRepo(REPO_ID); + testDb.addCommit( + REPO_ID, + COMM_A_HASH, + true, + true, + true, + "aA", + Instant.ofEpochSecond(1600010001), + "cA", + Instant.ofEpochSecond(1600010002), + "A"); + testDb.addCommit( + REPO_ID, + COMM_B_HASH, + true, + true, + true, + "aB", + Instant.ofEpochSecond(1600020001), + "cB", + Instant.ofEpochSecond(1600020002), + "B"); + testDb.addCommit( + REPO_ID, + COMM_C_HASH, + true, + true, + true, + "aC", + Instant.ofEpochSecond(1600030001), + "cC", + Instant.ofEpochSecond(1600030002), + "C"); + testDb.addCommit( + REPO_ID, + COMM_D_HASH, + true, + true, + true, + "aD", + Instant.ofEpochSecond(1600040001), + "cD", + Instant.ofEpochSecond(1600040002), + "D"); + testDb.addCommit( + REPO_ID, + COMM_E_HASH, + true, + true, + true, + "aE", + Instant.ofEpochSecond(1600050001), + "cE", + Instant.ofEpochSecond(1600050002), + "E"); + testDb.addCommit( + REPO_ID, + COMM_F_HASH, + true, + false, + true, + "aF", + Instant.ofEpochSecond(1600060001), + "cF", + Instant.ofEpochSecond(1600060002), + "F"); + testDb.addCommit( + REPO_ID, + COMM_G_HASH, + true, + false, + true, + "aG", + Instant.ofEpochSecond(1600070001), + "cG", + Instant.ofEpochSecond(1600070002), + "G"); + testDb.addCommit( + REPO_ID, + COMM_H_HASH, + false, + false, + true, + "aH", + Instant.ofEpochSecond(1600080001), + "cH", + Instant.ofEpochSecond(1600080002), + "H"); + testDb.addCommit( + REPO_ID, + COMM_I_HASH, + false, + false, + true, + "aI", + Instant.ofEpochSecond(1600090001), + "cI", + Instant.ofEpochSecond(1600090002), + "I"); + testDb.addCommitRel(REPO_ID, COMM_A_HASH, COMM_B_HASH); + testDb.addCommitRel(REPO_ID, COMM_B_HASH, COMM_C_HASH); + testDb.addCommitRel(REPO_ID, COMM_B_HASH, COMM_F_HASH); + testDb.addCommitRel(REPO_ID, COMM_C_HASH, COMM_D_HASH); + testDb.addCommitRel(REPO_ID, COMM_D_HASH, COMM_E_HASH); + testDb.addCommitRel(REPO_ID, COMM_D_HASH, COMM_F_HASH); + testDb.addCommitRel(REPO_ID, COMM_D_HASH, COMM_H_HASH); + testDb.addCommitRel(REPO_ID, COMM_F_HASH, COMM_G_HASH); + testDb.addCommitRel(REPO_ID, COMM_F_HASH, COMM_I_HASH); + testDb.addBranch(REPO_ID, BRANCH_T, COMM_E_HASH, true); + testDb.addBranch(REPO_ID, BRANCH_U, COMM_G_HASH, false); + + DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + access = new CommitReadAccess(databaseStorage); + } + + private void checkCommit( + Commit commit, + RepoId repoId, + CommitHash commitHash, + boolean isReachable, + boolean isTracked, + String author, + int authorDate, + String committer, + int committerDate, + String message) { + + assertThat(commit.getRepoId()).isEqualTo(repoId); + assertThat(commit.getHash()).isEqualTo(commitHash); + assertThat(commit.isReachable()).isEqualTo(isReachable); + assertThat(commit.isTracked()).isEqualTo(isTracked); + assertThat(commit.getAuthor()).isEqualTo(author); + assertThat(commit.getAuthorDate()).isEqualTo(Instant.ofEpochSecond(authorDate)); + assertThat(commit.getCommitter()).isEqualTo(committer); + assertThat(commit.getCommitterDate()).isEqualTo(Instant.ofEpochSecond(committerDate)); + assertThat(commit.getMessage()).isEqualTo(message); + } + + private void checkFullCommit( + FullCommit commit, + RepoId repoId, + CommitHash commitHash, + boolean isReachable, + boolean isTracked, + String author, + int authorDate, + String committer, + int committerDate, + String message, + List parents, + List children) { + + checkCommit( + commit, + repoId, + commitHash, + isReachable, + isTracked, + author, + authorDate, + committer, + committerDate, + message); + + assertThat(commit.getParentHashes()).containsExactlyInAnyOrderElementsOf(parents); + assertThat(commit.getChildHashes()).containsExactlyInAnyOrderElementsOf(children); + } + + @Test + void getCommit() { + checkCommit( + access.getCommit(REPO_ID, COMM_A_HASH), + REPO_ID, + COMM_A_HASH, + true, + true, + "aA", + 1600010001, + "cA", + 1600010002, + "A"); + checkCommit( + access.getCommit(REPO_ID, COMM_B_HASH), + REPO_ID, + COMM_B_HASH, + true, + true, + "aB", + 1600020001, + "cB", + 1600020002, + "B"); + checkCommit( + access.getCommit(REPO_ID, COMM_C_HASH), + REPO_ID, + COMM_C_HASH, + true, + true, + "aC", + 1600030001, + "cC", + 1600030002, + "C"); + checkCommit( + access.getCommit(REPO_ID, COMM_D_HASH), + REPO_ID, + COMM_D_HASH, + true, + true, + "aD", + 1600040001, + "cD", + 1600040002, + "D"); + checkCommit( + access.getCommit(REPO_ID, COMM_E_HASH), + REPO_ID, + COMM_E_HASH, + true, + true, + "aE", + 1600050001, + "cE", + 1600050002, + "E"); + checkCommit( + access.getCommit(REPO_ID, COMM_F_HASH), + REPO_ID, + COMM_F_HASH, + true, + false, + "aF", + 1600060001, + "cF", + 1600060002, + "F"); + checkCommit( + access.getCommit(REPO_ID, COMM_G_HASH), + REPO_ID, + COMM_G_HASH, + true, + false, + "aG", + 1600070001, + "cG", + 1600070002, + "G"); + checkCommit( + access.getCommit(REPO_ID, COMM_H_HASH), + REPO_ID, + COMM_H_HASH, + false, + false, + "aH", + 1600080001, + "cH", + 1600080002, + "H"); + checkCommit( + access.getCommit(REPO_ID, COMM_I_HASH), + REPO_ID, + COMM_I_HASH, + false, + false, + "aI", + 1600090001, + "cI", + 1600090002, + "I"); + + RepoId nonexistentId = new RepoId(); + CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); + + assertThatThrownBy(() -> access.getCommit(REPO_ID, nonexistentHash)) + .isInstanceOf(NoSuchCommitException.class) + .satisfies( + throwable -> { + NoSuchCommitException e = (NoSuchCommitException) throwable; + assertThat(e.getRepoId()).isEqualTo(REPO_ID); + assertThat(e.getCommitHash()).isEqualTo(nonexistentHash); + }); + + assertThatThrownBy(() -> access.getCommit(nonexistentId, COMM_A_HASH)) + .isInstanceOf(NoSuchCommitException.class) + .satisfies( + throwable -> { + NoSuchCommitException e = (NoSuchCommitException) throwable; + assertThat(e.getRepoId()).isEqualTo(nonexistentId); + assertThat(e.getCommitHash()).isEqualTo(COMM_A_HASH); + }); + } + + @Test + void guarding() { + RepoId nonexistentId = new RepoId(); + CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); + + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_A_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_B_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_C_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_D_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_E_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_F_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_G_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_H_HASH)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardCommitExists(REPO_ID, COMM_I_HASH)).doesNotThrowAnyException(); + + assertThatThrownBy(() -> access.guardCommitExists(REPO_ID, nonexistentHash)) + .isInstanceOf(NoSuchCommitException.class) + .satisfies( + throwable -> { + NoSuchCommitException e = (NoSuchCommitException) throwable; + assertThat(e.getRepoId()).isEqualTo(REPO_ID); + assertThat(e.getCommitHash()).isEqualTo(nonexistentHash); + }); + + assertThatThrownBy(() -> access.guardCommitExists(nonexistentId, COMM_A_HASH)) + .isInstanceOf(NoSuchCommitException.class) + .satisfies( + throwable -> { + NoSuchCommitException e = (NoSuchCommitException) throwable; + assertThat(e.getRepoId()).isEqualTo(nonexistentId); + assertThat(e.getCommitHash()).isEqualTo(COMM_A_HASH); + }); + } + + @Test + void getCommits() { + RepoId nonexistentId = new RepoId(); + CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); + + List commits = + access.getCommits(REPO_ID, List.of(COMM_B_HASH, COMM_G_HASH, COMM_I_HASH, nonexistentHash)); + + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_G_HASH, COMM_I_HASH); + + assertThat(commits.stream().map(Commit::getRepoId).collect(toSet())) + .containsExactlyInAnyOrder(REPO_ID); + + Map commitMap = commits.stream().collect(toMap(Commit::getHash, it -> it)); + + checkCommit( + commitMap.get(COMM_B_HASH), + REPO_ID, + COMM_B_HASH, + true, + true, + "aB", + 1600020001, + "cB", + 1600020002, + "B"); + checkCommit( + commitMap.get(COMM_G_HASH), + REPO_ID, + COMM_G_HASH, + true, + false, + "aG", + 1600070001, + "cG", + 1600070002, + "G"); + checkCommit( + commitMap.get(COMM_I_HASH), + REPO_ID, + COMM_I_HASH, + false, + false, + "aI", + 1600090001, + "cI", + 1600090002, + "I"); + + assertThat(access.getCommits(nonexistentId, List.of(COMM_A_HASH, COMM_E_HASH, COMM_I_HASH))) + .isEmpty(); + } + + @Test + void getFullCommit() { + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_A_HASH), + REPO_ID, + COMM_A_HASH, + true, + true, + "aA", + 1600010001, + "cA", + 1600010002, + "A", + List.of(), + List.of(COMM_B_HASH)); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_B_HASH), + REPO_ID, + COMM_B_HASH, + true, + true, + "aB", + 1600020001, + "cB", + 1600020002, + "B", + List.of(COMM_A_HASH), + List.of(COMM_C_HASH, COMM_F_HASH)); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_C_HASH), + REPO_ID, + COMM_C_HASH, + true, + true, + "aC", + 1600030001, + "cC", + 1600030002, + "C", + List.of(COMM_B_HASH), + List.of(COMM_D_HASH)); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_D_HASH), + REPO_ID, + COMM_D_HASH, + true, + true, + "aD", + 1600040001, + "cD", + 1600040002, + "D", + List.of(COMM_C_HASH), + List.of(COMM_E_HASH, COMM_F_HASH, COMM_H_HASH)); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_E_HASH), + REPO_ID, + COMM_E_HASH, + true, + true, + "aE", + 1600050001, + "cE", + 1600050002, + "E", + List.of(COMM_D_HASH), + List.of()); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_F_HASH), + REPO_ID, + COMM_F_HASH, + true, + false, + "aF", + 1600060001, + "cF", + 1600060002, + "F", + List.of(COMM_B_HASH, COMM_D_HASH), + List.of(COMM_G_HASH, COMM_I_HASH)); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_G_HASH), + REPO_ID, + COMM_G_HASH, + true, + false, + "aG", + 1600070001, + "cG", + 1600070002, + "G", + List.of(COMM_F_HASH), + List.of()); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_H_HASH), + REPO_ID, + COMM_H_HASH, + false, + false, + "aH", + 1600080001, + "cH", + 1600080002, + "H", + List.of(COMM_D_HASH), + List.of()); + checkFullCommit( + access.getFullCommit(REPO_ID, COMM_I_HASH), + REPO_ID, + COMM_I_HASH, + false, + false, + "aI", + 1600090001, + "cI", + 1600090002, + "I", + List.of(COMM_F_HASH), + List.of()); + + RepoId nonexistentId = new RepoId(); + CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); + + assertThatThrownBy(() -> access.getFullCommit(REPO_ID, nonexistentHash)) + .isInstanceOf(NoSuchCommitException.class) + .satisfies( + throwable -> { + NoSuchCommitException e = (NoSuchCommitException) throwable; + assertThat(e.getRepoId()).isEqualTo(REPO_ID); + assertThat(e.getCommitHash()).isEqualTo(nonexistentHash); + }); + + assertThatThrownBy(() -> access.getFullCommit(nonexistentId, COMM_A_HASH)) + .isInstanceOf(NoSuchCommitException.class) + .satisfies( + throwable -> { + NoSuchCommitException e = (NoSuchCommitException) throwable; + assertThat(e.getRepoId()).isEqualTo(nonexistentId); + assertThat(e.getCommitHash()).isEqualTo(COMM_A_HASH); + }); + } + + @Test + void promoteCommits() { + CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); + + // Missing COMM_I_HASH and including one nonexistent hash + List commits = + access.promoteCommits( + access.getCommits( + REPO_ID, + List.of( + COMM_A_HASH, + COMM_B_HASH, + COMM_C_HASH, + COMM_D_HASH, + COMM_E_HASH, + COMM_F_HASH, + COMM_G_HASH, + COMM_H_HASH, + nonexistentHash))); + + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder( + COMM_A_HASH, + COMM_B_HASH, + COMM_C_HASH, + COMM_D_HASH, + COMM_E_HASH, + COMM_F_HASH, + COMM_G_HASH, + COMM_H_HASH); + + Map commitMap = + commits.stream().collect(toMap(Commit::getHash, it -> it)); + + checkFullCommit( + commitMap.get(COMM_A_HASH), + REPO_ID, + COMM_A_HASH, + true, + true, + "aA", + 1600010001, + "cA", + 1600010002, + "A", + List.of(), + List.of(COMM_B_HASH)); + checkFullCommit( + commitMap.get(COMM_B_HASH), + REPO_ID, + COMM_B_HASH, + true, + true, + "aB", + 1600020001, + "cB", + 1600020002, + "B", + List.of(COMM_A_HASH), + List.of(COMM_C_HASH, COMM_F_HASH)); + checkFullCommit( + commitMap.get(COMM_C_HASH), + REPO_ID, + COMM_C_HASH, + true, + true, + "aC", + 1600030001, + "cC", + 1600030002, + "C", + List.of(COMM_B_HASH), + List.of(COMM_D_HASH)); + checkFullCommit( + commitMap.get(COMM_D_HASH), + REPO_ID, + COMM_D_HASH, + true, + true, + "aD", + 1600040001, + "cD", + 1600040002, + "D", + List.of(COMM_C_HASH), + List.of(COMM_E_HASH, COMM_F_HASH, COMM_H_HASH)); + checkFullCommit( + commitMap.get(COMM_E_HASH), + REPO_ID, + COMM_E_HASH, + true, + true, + "aE", + 1600050001, + "cE", + 1600050002, + "E", + List.of(COMM_D_HASH), + List.of()); + checkFullCommit( + commitMap.get(COMM_F_HASH), + REPO_ID, + COMM_F_HASH, + true, + false, + "aF", + 1600060001, + "cF", + 1600060002, + "F", + List.of(COMM_B_HASH, COMM_D_HASH), + List.of(COMM_G_HASH, COMM_I_HASH)); + checkFullCommit( + commitMap.get(COMM_G_HASH), + REPO_ID, + COMM_G_HASH, + true, + false, + "aG", + 1600070001, + "cG", + 1600070002, + "G", + List.of(COMM_F_HASH), + List.of()); + checkFullCommit( + commitMap.get(COMM_H_HASH), + REPO_ID, + COMM_H_HASH, + false, + false, + "aH", + 1600080001, + "cH", + 1600080002, + "H", + List.of(COMM_D_HASH), + List.of()); + } + + @Test + void getParentsAndChildren() { + assertThat(access.getParentHashes(REPO_ID, COMM_A_HASH)).isEmpty(); + assertThat(access.getParentHashes(REPO_ID, COMM_B_HASH)).containsExactlyInAnyOrder(COMM_A_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_C_HASH)).containsExactlyInAnyOrder(COMM_B_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_D_HASH)).containsExactlyInAnyOrder(COMM_C_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_E_HASH)).containsExactlyInAnyOrder(COMM_D_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_F_HASH)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_G_HASH)).containsExactlyInAnyOrder(COMM_F_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_H_HASH)).containsExactlyInAnyOrder(COMM_D_HASH); + assertThat(access.getParentHashes(REPO_ID, COMM_I_HASH)).containsExactlyInAnyOrder(COMM_F_HASH); + + assertThat(access.getChildHashes(REPO_ID, COMM_A_HASH)).containsExactlyInAnyOrder(COMM_B_HASH); + assertThat(access.getChildHashes(REPO_ID, COMM_B_HASH)) + .containsExactlyInAnyOrder(COMM_C_HASH, COMM_F_HASH); + assertThat(access.getChildHashes(REPO_ID, COMM_C_HASH)).containsExactlyInAnyOrder(COMM_D_HASH); + assertThat(access.getChildHashes(REPO_ID, COMM_D_HASH)) + .containsExactlyInAnyOrder(COMM_E_HASH, COMM_F_HASH, COMM_H_HASH); + assertThat(access.getChildHashes(REPO_ID, COMM_E_HASH)).isEmpty(); + assertThat(access.getChildHashes(REPO_ID, COMM_F_HASH)) + .containsExactlyInAnyOrder(COMM_G_HASH, COMM_I_HASH); + assertThat(access.getChildHashes(REPO_ID, COMM_G_HASH)).isEmpty(); + assertThat(access.getChildHashes(REPO_ID, COMM_H_HASH)).isEmpty(); + assertThat(access.getChildHashes(REPO_ID, COMM_I_HASH)).isEmpty(); + } + + @Test + void getDescendantCommits() { + assertThat(access.getDescendantCommits(REPO_ID, COMM_A_HASH)) + .containsExactlyInAnyOrder(COMM_A_HASH, COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); + assertThat(access.getDescendantCommits(REPO_ID, COMM_B_HASH)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); + assertThat(access.getDescendantCommits(REPO_ID, COMM_C_HASH)) + .containsExactlyInAnyOrder(COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); + assertThat(access.getDescendantCommits(REPO_ID, COMM_D_HASH)) + .containsExactlyInAnyOrder(COMM_D_HASH, COMM_E_HASH); + assertThat(access.getDescendantCommits(REPO_ID, COMM_E_HASH)) + .containsExactlyInAnyOrder(COMM_E_HASH); + assertThat(access.getDescendantCommits(REPO_ID, COMM_F_HASH)).isEmpty(); + assertThat(access.getDescendantCommits(REPO_ID, COMM_G_HASH)).isEmpty(); + assertThat(access.getDescendantCommits(REPO_ID, COMM_H_HASH)).isEmpty(); + assertThat(access.getDescendantCommits(REPO_ID, COMM_I_HASH)).isEmpty(); + + RepoId nonexistentId = new RepoId(); + CommitHash nonexistentHash = new CommitHash("f49028fa485c0bcda6104c8c8b4e97addbde7079"); + + assertThat(access.getDescendantCommits(nonexistentId, COMM_B_HASH)).isEmpty(); + assertThat(access.getDescendantCommits(REPO_ID, nonexistentHash)).isEmpty(); + } + + @Test + void getTrackedCommitsBetween() { + // Check that untracked and unreachable commits are not included + + List commits = + access.getTrackedCommitsBetween( + REPO_ID, Instant.ofEpochSecond(1600030000), Instant.ofEpochSecond(1600080005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); + + // Check off-by-one errors and that committer and not author time is used + + commits = + access.getTrackedCommitsBetween( + REPO_ID, Instant.ofEpochSecond(1600040003), Instant.ofEpochSecond(1600050002)); + assertThat(commits.stream().map(Commit::getHash)).containsExactlyInAnyOrder(COMM_E_HASH); + + commits = + access.getTrackedCommitsBetween( + REPO_ID, Instant.ofEpochSecond(1600040002), Instant.ofEpochSecond(1600050001)); + assertThat(commits.stream().map(Commit::getHash)).containsExactlyInAnyOrder(COMM_D_HASH); + } + + @Test + void getFirstParentOfBranch() { + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_A_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_B_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_C_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_D_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_E_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_F_HASH)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_G_HASH)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_H_HASH)) + .containsExactlyInAnyOrder(COMM_D_HASH); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_T, COMM_I_HASH)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_D_HASH); + + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_A_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_B_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_C_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_D_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_E_HASH)) + .containsExactlyInAnyOrder(COMM_D_HASH); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_F_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_G_HASH)).isEmpty(); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_H_HASH)) + .containsExactlyInAnyOrder(COMM_D_HASH); + assertThat(access.getFirstParentsOfBranch(REPO_ID, BRANCH_U, COMM_I_HASH)) + .containsExactlyInAnyOrder(COMM_F_HASH); + } + + @Test + void getCommitsBetween() { + // From an untracked branch + + List commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("U")), + Instant.ofEpochSecond(1600020000), + Instant.ofEpochSecond(1600060005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_F_HASH); + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("U")), + Instant.ofEpochSecond(1600020000), + Instant.ofEpochSecond(1600090005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_F_HASH, COMM_G_HASH); + + // From a tracked branch + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T")), + Instant.ofEpochSecond(1600020000), + Instant.ofEpochSecond(1600040005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH); + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T")), + Instant.ofEpochSecond(1600020000), + Instant.ofEpochSecond(1600090005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH); + + // From both branches + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T"), BranchName.fromName("U")), + Instant.ofEpochSecond(1600020000), + Instant.ofEpochSecond(1600060005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH, COMM_F_HASH); + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T"), BranchName.fromName("U")), + Instant.ofEpochSecond(1600020000), + Instant.ofEpochSecond(1600090005)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder( + COMM_B_HASH, COMM_C_HASH, COMM_D_HASH, COMM_E_HASH, COMM_F_HASH, COMM_G_HASH); + + // Check off-by-one errors and that committer and not author time is used + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T")), + Instant.ofEpochSecond(1600040002), + Instant.ofEpochSecond(1600050001)); + assertThat(commits.stream().map(Commit::getHash)).containsExactlyInAnyOrder(COMM_D_HASH); + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T")), + Instant.ofEpochSecond(1600040001), + Instant.ofEpochSecond(1600050002)); + assertThat(commits.stream().map(Commit::getHash)) + .containsExactlyInAnyOrder(COMM_D_HASH, COMM_E_HASH); + + commits = + access.getCommitsBetween( + REPO_ID, + List.of(BranchName.fromName("T")), + Instant.ofEpochSecond(1600040003), + Instant.ofEpochSecond(1600050000)); + assertThat(commits).isEmpty(); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitTest.java index d0f282cdd..e8d6355c9 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/committaccess/entities/CommitTest.java @@ -9,113 +9,110 @@ class CommitTest { - private Commit commitWithMessage(String message) { - return new Commit( - new RepoId(), - new CommitHash("8853e6aaf9b6721205d388285fb42bff89b6ec38"), - true, - true, - "author", - Instant.now(), - "committer", - Instant.now(), - message - ); - } - - @Test - void parseMessageCorrectly() { - // Single- and multiline messages with only a summary - - Commit commit = commitWithMessage("hello"); - assertThat(commit.getMessage()).isEqualTo("hello"); - assertThat(commit.getSummary()).isEqualTo("hello"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); - - commit = commitWithMessage("hello\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); - - commit = commitWithMessage("hello\nworld"); - assertThat(commit.getMessage()).isEqualTo("hello\nworld"); - assertThat(commit.getSummary()).isEqualTo("hello\nworld"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); - - commit = commitWithMessage("hello\nworld\n"); - assertThat(commit.getMessage()).isEqualTo("hello\nworld\n"); - assertThat(commit.getSummary()).isEqualTo("hello\nworld\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); - - // Single- and multiline messages with existing but empty message body - - commit = commitWithMessage("hello\nworld\n\n"); - assertThat(commit.getMessage()).isEqualTo("hello\nworld\n\n"); - assertThat(commit.getSummary()).isEqualTo("hello\nworld\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("")); - - commit = commitWithMessage("hello\n\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("")); - - // Messages with summary and body - // This section is mainly about preserving newlines - - commit = commitWithMessage("hello\n\nworld"); - assertThat(commit.getMessage()).isEqualTo("hello\n\nworld"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world")); - - commit = commitWithMessage("hello\n\nworld\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world\n")); - - commit = commitWithMessage("hello\n\nworld\n\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\n\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world\n\n")); - - commit = commitWithMessage("hello\n\n\nworld"); - assertThat(commit.getMessage()).isEqualTo("hello\n\n\nworld"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("\nworld")); - - commit = commitWithMessage("hello\n\n\nworld\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n\n\nworld\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("\nworld\n")); - - commit = commitWithMessage("hello\n\n\nworld\n\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n\n\nworld\n\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("\nworld\n\n")); - - // Multiline summaries and bodies - - commit = commitWithMessage("hello\n\nworld\nmultiline\nmessage"); - assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\nmultiline\nmessage"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()) - .isEqualTo(Optional.of("world\nmultiline\nmessage")); - - commit = commitWithMessage("hello\n\nworld\nmultiline\nmessage\n"); - assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\nmultiline\nmessage\n"); - assertThat(commit.getSummary()).isEqualTo("hello\n"); - assertThat(commit.getMessageWithoutSummary()) - .isEqualTo(Optional.of("world\nmultiline\nmessage\n")); - - commit = commitWithMessage("hello\nmultiline\nmessage\n\nworld"); - assertThat(commit.getMessage()).isEqualTo("hello\nmultiline\nmessage\n\nworld"); - assertThat(commit.getSummary()).isEqualTo("hello\nmultiline\nmessage\n"); - assertThat(commit.getMessageWithoutSummary()) - .isEqualTo(Optional.of("world")); - - commit = commitWithMessage("hello\nmultiline\nmessage\n\nworld\n"); - assertThat(commit.getMessage()).isEqualTo("hello\nmultiline\nmessage\n\nworld\n"); - assertThat(commit.getSummary()).isEqualTo("hello\nmultiline\nmessage\n"); - assertThat(commit.getMessageWithoutSummary()) - .isEqualTo(Optional.of("world\n")); - } + private Commit commitWithMessage(String message) { + return new Commit( + new RepoId(), + new CommitHash("8853e6aaf9b6721205d388285fb42bff89b6ec38"), + true, + true, + "author", + Instant.now(), + "committer", + Instant.now(), + message); + } + + @Test + void parseMessageCorrectly() { + // Single- and multiline messages with only a summary + + Commit commit = commitWithMessage("hello"); + assertThat(commit.getMessage()).isEqualTo("hello"); + assertThat(commit.getSummary()).isEqualTo("hello"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); + + commit = commitWithMessage("hello\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); + + commit = commitWithMessage("hello\nworld"); + assertThat(commit.getMessage()).isEqualTo("hello\nworld"); + assertThat(commit.getSummary()).isEqualTo("hello\nworld"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); + + commit = commitWithMessage("hello\nworld\n"); + assertThat(commit.getMessage()).isEqualTo("hello\nworld\n"); + assertThat(commit.getSummary()).isEqualTo("hello\nworld\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.empty()); + + // Single- and multiline messages with existing but empty message body + + commit = commitWithMessage("hello\nworld\n\n"); + assertThat(commit.getMessage()).isEqualTo("hello\nworld\n\n"); + assertThat(commit.getSummary()).isEqualTo("hello\nworld\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("")); + + commit = commitWithMessage("hello\n\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("")); + + // Messages with summary and body + // This section is mainly about preserving newlines + + commit = commitWithMessage("hello\n\nworld"); + assertThat(commit.getMessage()).isEqualTo("hello\n\nworld"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world")); + + commit = commitWithMessage("hello\n\nworld\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world\n")); + + commit = commitWithMessage("hello\n\nworld\n\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\n\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world\n\n")); + + commit = commitWithMessage("hello\n\n\nworld"); + assertThat(commit.getMessage()).isEqualTo("hello\n\n\nworld"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("\nworld")); + + commit = commitWithMessage("hello\n\n\nworld\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n\n\nworld\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("\nworld\n")); + + commit = commitWithMessage("hello\n\n\nworld\n\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n\n\nworld\n\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("\nworld\n\n")); + + // Multiline summaries and bodies + + commit = commitWithMessage("hello\n\nworld\nmultiline\nmessage"); + assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\nmultiline\nmessage"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()) + .isEqualTo(Optional.of("world\nmultiline\nmessage")); + + commit = commitWithMessage("hello\n\nworld\nmultiline\nmessage\n"); + assertThat(commit.getMessage()).isEqualTo("hello\n\nworld\nmultiline\nmessage\n"); + assertThat(commit.getSummary()).isEqualTo("hello\n"); + assertThat(commit.getMessageWithoutSummary()) + .isEqualTo(Optional.of("world\nmultiline\nmessage\n")); + + commit = commitWithMessage("hello\nmultiline\nmessage\n\nworld"); + assertThat(commit.getMessage()).isEqualTo("hello\nmultiline\nmessage\n\nworld"); + assertThat(commit.getSummary()).isEqualTo("hello\nmultiline\nmessage\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world")); + + commit = commitWithMessage("hello\nmultiline\nmessage\n\nworld\n"); + assertThat(commit.getMessage()).isEqualTo("hello\nmultiline\nmessage\n\nworld\n"); + assertThat(commit.getSummary()).isEqualTo("hello\nmultiline\nmessage\n"); + assertThat(commit.getMessageWithoutSummary()).isEqualTo(Optional.of("world\n")); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccessTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccessTest.java index a84a76d3f..d07191a39 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccessTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/DimensionReadAccessTest.java @@ -30,202 +30,194 @@ class DimensionReadAccessTest { - private static final List DIMENSIONS = List.of( - new DimensionInfo(new Dimension("a", "b"), new Unit("c"), Interpretation.LESS_IS_BETTER, true), - new DimensionInfo(new Dimension("a", "d"), new Unit("e"), Interpretation.MORE_IS_BETTER, false), - new DimensionInfo(new Dimension("f", "g"), new Unit("h"), Interpretation.NEUTRAL, true), - new DimensionInfo(new Dimension("i", "j"), new Unit("k"), Interpretation.NEUTRAL, false) - ); - - private static final RepoId REPO1_ID = new RepoId(); - private static final RepoId REPO2_ID = new RepoId(); - private static final RepoId REPO3_ID = new RepoId(); - private static final RepoId REPO4_ID = new RepoId(); - private static final RunId RUN1_ID = new RunId(); - private static final RunId RUN2_ID = new RunId(); - private static final RunId RUN3_ID = new RunId(); - private static final RunId RUN4_ID = new RunId(); - - private DatabaseStorage databaseStorage; - private DimensionReadAccess access; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - TestDb testDb = new TestDb(tempDir); - - DIMENSIONS.forEach(testDb::addDimension); - - testDb.addRepo(REPO1_ID); - testDb.addRepo(REPO2_ID); - testDb.addRepo(REPO3_ID); - testDb.addRepo(REPO4_ID); - CommitHash run1Hash = new CommitHash("2acba5b560711dc4c8e53c356238862c07712eca"); - CommitHash run2Hash = new CommitHash("5dd4f1f6f0b3d5d5d830c6e4789d6f161496fa81"); - CommitHash run3Hash = new CommitHash("0bb703003ec44ac609760876a4668ff5a05b512b"); - CommitHash run4Hash = new CommitHash("a23577b29bfe6e384bba835b4453d0fc7f33855c"); - testDb.addCommit(REPO1_ID, run1Hash); - testDb.addCommit(REPO2_ID, run2Hash); - testDb.addCommit(REPO2_ID, run3Hash); - testDb.addCommit(REPO3_ID, run4Hash); - testDb.addRun(RUN1_ID, Either.ofLeft(new CommitSource(REPO1_ID, run1Hash))); - testDb.addRun(RUN2_ID, Either.ofLeft(new CommitSource(REPO2_ID, run2Hash))); - testDb.addRun(RUN3_ID, Either.ofLeft(new CommitSource(REPO2_ID, run3Hash))); - testDb.addRun(RUN4_ID, Either.ofLeft(new CommitSource(REPO3_ID, run4Hash))); - testDb.addMeasurement(RUN1_ID, new Dimension("a", "b")); - testDb.addMeasurement(RUN1_ID, new Dimension("f", "g")); - testDb.addMeasurement(RUN2_ID, new Dimension("a", "d")); - testDb.addMeasurement(RUN3_ID, new Dimension("f", "g")); - testDb.addMeasurement(RUN4_ID, new Dimension("i", "j")); - - databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - access = new DimensionReadAccess(databaseStorage); - } - - @Test - void guardDimensionExists() { - assertThatCode(() -> access.guardDimensionExists(new Dimension("a", "b"))) - .doesNotThrowAnyException(); - assertThatCode(() -> access.guardDimensionExists(new Dimension("a", "d"))) - .doesNotThrowAnyException(); - assertThatCode(() -> access.guardDimensionExists(new Dimension("f", "g"))) - .doesNotThrowAnyException(); - assertThatCode(() -> access.guardDimensionExists(new Dimension("i", "j"))) - .doesNotThrowAnyException(); - - assertThatThrownBy(() -> access.guardDimensionExists(new Dimension("x", "y"))) - .isInstanceOf(NoSuchDimensionException.class) - .extracting("invalidDimension") - .isEqualTo(new Dimension("x", "y")); - - assertThatThrownBy(() -> access.guardDimensionExists(new Dimension("a", "g"))) - .isInstanceOf(NoSuchDimensionException.class) - .extracting("invalidDimension") - .isEqualTo(new Dimension("a", "g")); - } - - @Test - void getDimensions() { - // All dimensions - Set allDimensions = access.getAllDimensions(); - assertThat(allDimensions.stream() - .map(DimensionInfo::getDimension)) - .containsExactlyInAnyOrder( - new Dimension("a", "b"), - new Dimension("a", "d"), - new Dimension("f", "g"), - new Dimension("i", "j") - ); - - Map allDimensionsMap = allDimensions.stream() - .collect(toMap(DimensionInfo::getDimension, it -> it)); - - DimensionInfo infoAB = allDimensionsMap.get(new Dimension("a", "b")); - assertThat(infoAB.getUnit()).isEqualTo(new Unit("c")); - assertThat(infoAB.getInterpretation()).isEqualTo(Interpretation.LESS_IS_BETTER); - assertThat(infoAB.isSignificant()).isTrue(); - - DimensionInfo infoAD = allDimensionsMap.get(new Dimension("a", "d")); - assertThat(infoAD.getUnit()).isEqualTo(new Unit("e")); - assertThat(infoAD.getInterpretation()).isEqualTo(Interpretation.MORE_IS_BETTER); - assertThat(infoAD.isSignificant()).isFalse(); - - DimensionInfo infoFG = allDimensionsMap.get(new Dimension("f", "g")); - assertThat(infoFG.getUnit()).isEqualTo(new Unit("h")); - assertThat(infoFG.getInterpretation()).isEqualTo(Interpretation.NEUTRAL); - assertThat(infoFG.isSignificant()).isTrue(); - - DimensionInfo infoIJ = allDimensionsMap.get(new Dimension("i", "j")); - assertThat(infoIJ.getUnit()).isEqualTo(new Unit("k")); - assertThat(infoIJ.getInterpretation()).isEqualTo(Interpretation.NEUTRAL); - assertThat(infoIJ.isSignificant()).isFalse(); - - // Significant dimensions - assertThat(access.getSignificantDimensions()) - .containsExactlyInAnyOrder( - new Dimension("a", "b"), - new Dimension("f", "g") - ); - } - - @Test - void getDimensionInfo() { - DimensionInfo infoAB = access.getDimensionInfo(new Dimension("a", "b")); - assertThat(infoAB.getUnit()).isEqualTo(new Unit("c")); - assertThat(infoAB.getInterpretation()).isEqualTo(Interpretation.LESS_IS_BETTER); - assertThat(infoAB.isSignificant()).isTrue(); - - // Nonexistent info has default values - DimensionInfo infoXY = access.getDimensionInfo(new Dimension("x", "y")); - assertThat(infoXY.getUnit()).isEqualTo(Unit.DEFAULT); - assertThat(infoXY.getInterpretation()).isEqualTo(Interpretation.DEFAULT); - assertThat(infoXY.isSignificant()).isTrue(); // Default significance - } - - @Test - void getDimensionInfos() { - Set infos = access.getDimensionInfos(Set.of(new Dimension("a", "d"))); - assertThat(infos.stream() - .map(DimensionInfo::getDimension)) - .containsExactly(new Dimension("a", "d")); - - DimensionInfo infoAD = new ArrayList<>(infos).get(0); - assertThat(infoAD.getUnit()).isEqualTo(new Unit("e")); - assertThat(infoAD.getInterpretation()).isEqualTo(Interpretation.MORE_IS_BETTER); - assertThat(infoAD.isSignificant()).isFalse(); - - Map infoMap = access - .getDimensionInfoMap(Set.of(new Dimension("a", "b"), new Dimension("f", "g"))); - assertThat(infoMap).containsOnlyKeys(new Dimension("a", "b"), new Dimension("f", "g")); - - DimensionInfo infoAB = infoMap.get(new Dimension("a", "b")); - assertThat(infoAB.getUnit()).isEqualTo(new Unit("c")); - assertThat(infoAB.getInterpretation()).isEqualTo(Interpretation.LESS_IS_BETTER); - assertThat(infoAB.isSignificant()).isTrue(); - - DimensionInfo infoFG = infoMap.get(new Dimension("f", "g")); - assertThat(infoFG.getUnit()).isEqualTo(new Unit("h")); - assertThat(infoFG.getInterpretation()).isEqualTo(Interpretation.NEUTRAL); - assertThat(infoFG.isSignificant()).isTrue(); - } - - @Test - void getAvailableDimensions() { - assertThat(access.getAvailableDimensions(REPO1_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); - assertThat(access.getAvailableDimensions(REPO2_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "d"), new Dimension("f", "g")); - - Map> dimensions = access - .getAvailableDimensions(List.of(REPO1_ID, REPO2_ID)); - assertThat(dimensions) - .containsOnlyKeys(REPO1_ID, REPO2_ID); - assertThat(dimensions.get(REPO1_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); - assertThat(dimensions.get(REPO2_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "d"), new Dimension("f", "g")); - - // Change run3's source to a tar source - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl() - .update(RUN) - .set(RUN.COMMIT_HASH, (String) null) - .set(RUN.TAR_DESC, "tarDesc") - .where(RUN.ID.eq(RUN3_ID.getIdAsString())) - .execute(); - } - - assertThat(access.getAvailableDimensions(REPO1_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); - assertThat(access.getAvailableDimensions(REPO2_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "d")); - - dimensions = access - .getAvailableDimensions(List.of(REPO1_ID, REPO2_ID)); - assertThat(dimensions) - .containsOnlyKeys(REPO1_ID, REPO2_ID); - assertThat(dimensions.get(REPO1_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); - assertThat(dimensions.get(REPO2_ID)) - .containsExactlyInAnyOrder(new Dimension("a", "d")); - } + private static final List DIMENSIONS = + List.of( + new DimensionInfo( + new Dimension("a", "b"), new Unit("c"), Interpretation.LESS_IS_BETTER, true), + new DimensionInfo( + new Dimension("a", "d"), new Unit("e"), Interpretation.MORE_IS_BETTER, false), + new DimensionInfo(new Dimension("f", "g"), new Unit("h"), Interpretation.NEUTRAL, true), + new DimensionInfo(new Dimension("i", "j"), new Unit("k"), Interpretation.NEUTRAL, false)); + + private static final RepoId REPO1_ID = new RepoId(); + private static final RepoId REPO2_ID = new RepoId(); + private static final RepoId REPO3_ID = new RepoId(); + private static final RepoId REPO4_ID = new RepoId(); + private static final RunId RUN1_ID = new RunId(); + private static final RunId RUN2_ID = new RunId(); + private static final RunId RUN3_ID = new RunId(); + private static final RunId RUN4_ID = new RunId(); + + private DatabaseStorage databaseStorage; + private DimensionReadAccess access; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + TestDb testDb = new TestDb(tempDir); + + DIMENSIONS.forEach(testDb::addDimension); + + testDb.addRepo(REPO1_ID); + testDb.addRepo(REPO2_ID); + testDb.addRepo(REPO3_ID); + testDb.addRepo(REPO4_ID); + CommitHash run1Hash = new CommitHash("2acba5b560711dc4c8e53c356238862c07712eca"); + CommitHash run2Hash = new CommitHash("5dd4f1f6f0b3d5d5d830c6e4789d6f161496fa81"); + CommitHash run3Hash = new CommitHash("0bb703003ec44ac609760876a4668ff5a05b512b"); + CommitHash run4Hash = new CommitHash("a23577b29bfe6e384bba835b4453d0fc7f33855c"); + testDb.addCommit(REPO1_ID, run1Hash); + testDb.addCommit(REPO2_ID, run2Hash); + testDb.addCommit(REPO2_ID, run3Hash); + testDb.addCommit(REPO3_ID, run4Hash); + testDb.addRun(RUN1_ID, Either.ofLeft(new CommitSource(REPO1_ID, run1Hash))); + testDb.addRun(RUN2_ID, Either.ofLeft(new CommitSource(REPO2_ID, run2Hash))); + testDb.addRun(RUN3_ID, Either.ofLeft(new CommitSource(REPO2_ID, run3Hash))); + testDb.addRun(RUN4_ID, Either.ofLeft(new CommitSource(REPO3_ID, run4Hash))); + testDb.addMeasurement(RUN1_ID, new Dimension("a", "b")); + testDb.addMeasurement(RUN1_ID, new Dimension("f", "g")); + testDb.addMeasurement(RUN2_ID, new Dimension("a", "d")); + testDb.addMeasurement(RUN3_ID, new Dimension("f", "g")); + testDb.addMeasurement(RUN4_ID, new Dimension("i", "j")); + + databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + access = new DimensionReadAccess(databaseStorage); + } + + @Test + void guardDimensionExists() { + assertThatCode(() -> access.guardDimensionExists(new Dimension("a", "b"))) + .doesNotThrowAnyException(); + assertThatCode(() -> access.guardDimensionExists(new Dimension("a", "d"))) + .doesNotThrowAnyException(); + assertThatCode(() -> access.guardDimensionExists(new Dimension("f", "g"))) + .doesNotThrowAnyException(); + assertThatCode(() -> access.guardDimensionExists(new Dimension("i", "j"))) + .doesNotThrowAnyException(); + + assertThatThrownBy(() -> access.guardDimensionExists(new Dimension("x", "y"))) + .isInstanceOf(NoSuchDimensionException.class) + .extracting("invalidDimension") + .isEqualTo(new Dimension("x", "y")); + + assertThatThrownBy(() -> access.guardDimensionExists(new Dimension("a", "g"))) + .isInstanceOf(NoSuchDimensionException.class) + .extracting("invalidDimension") + .isEqualTo(new Dimension("a", "g")); + } + + @Test + void getDimensions() { + // All dimensions + Set allDimensions = access.getAllDimensions(); + assertThat(allDimensions.stream().map(DimensionInfo::getDimension)) + .containsExactlyInAnyOrder( + new Dimension("a", "b"), + new Dimension("a", "d"), + new Dimension("f", "g"), + new Dimension("i", "j")); + + Map allDimensionsMap = + allDimensions.stream().collect(toMap(DimensionInfo::getDimension, it -> it)); + + DimensionInfo infoAB = allDimensionsMap.get(new Dimension("a", "b")); + assertThat(infoAB.getUnit()).isEqualTo(new Unit("c")); + assertThat(infoAB.getInterpretation()).isEqualTo(Interpretation.LESS_IS_BETTER); + assertThat(infoAB.isSignificant()).isTrue(); + + DimensionInfo infoAD = allDimensionsMap.get(new Dimension("a", "d")); + assertThat(infoAD.getUnit()).isEqualTo(new Unit("e")); + assertThat(infoAD.getInterpretation()).isEqualTo(Interpretation.MORE_IS_BETTER); + assertThat(infoAD.isSignificant()).isFalse(); + + DimensionInfo infoFG = allDimensionsMap.get(new Dimension("f", "g")); + assertThat(infoFG.getUnit()).isEqualTo(new Unit("h")); + assertThat(infoFG.getInterpretation()).isEqualTo(Interpretation.NEUTRAL); + assertThat(infoFG.isSignificant()).isTrue(); + + DimensionInfo infoIJ = allDimensionsMap.get(new Dimension("i", "j")); + assertThat(infoIJ.getUnit()).isEqualTo(new Unit("k")); + assertThat(infoIJ.getInterpretation()).isEqualTo(Interpretation.NEUTRAL); + assertThat(infoIJ.isSignificant()).isFalse(); + + // Significant dimensions + assertThat(access.getSignificantDimensions()) + .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); + } + + @Test + void getDimensionInfo() { + DimensionInfo infoAB = access.getDimensionInfo(new Dimension("a", "b")); + assertThat(infoAB.getUnit()).isEqualTo(new Unit("c")); + assertThat(infoAB.getInterpretation()).isEqualTo(Interpretation.LESS_IS_BETTER); + assertThat(infoAB.isSignificant()).isTrue(); + + // Nonexistent info has default values + DimensionInfo infoXY = access.getDimensionInfo(new Dimension("x", "y")); + assertThat(infoXY.getUnit()).isEqualTo(Unit.DEFAULT); + assertThat(infoXY.getInterpretation()).isEqualTo(Interpretation.DEFAULT); + assertThat(infoXY.isSignificant()).isTrue(); // Default significance + } + + @Test + void getDimensionInfos() { + Set infos = access.getDimensionInfos(Set.of(new Dimension("a", "d"))); + assertThat(infos.stream().map(DimensionInfo::getDimension)) + .containsExactly(new Dimension("a", "d")); + + DimensionInfo infoAD = new ArrayList<>(infos).get(0); + assertThat(infoAD.getUnit()).isEqualTo(new Unit("e")); + assertThat(infoAD.getInterpretation()).isEqualTo(Interpretation.MORE_IS_BETTER); + assertThat(infoAD.isSignificant()).isFalse(); + + Map infoMap = + access.getDimensionInfoMap(Set.of(new Dimension("a", "b"), new Dimension("f", "g"))); + assertThat(infoMap).containsOnlyKeys(new Dimension("a", "b"), new Dimension("f", "g")); + + DimensionInfo infoAB = infoMap.get(new Dimension("a", "b")); + assertThat(infoAB.getUnit()).isEqualTo(new Unit("c")); + assertThat(infoAB.getInterpretation()).isEqualTo(Interpretation.LESS_IS_BETTER); + assertThat(infoAB.isSignificant()).isTrue(); + + DimensionInfo infoFG = infoMap.get(new Dimension("f", "g")); + assertThat(infoFG.getUnit()).isEqualTo(new Unit("h")); + assertThat(infoFG.getInterpretation()).isEqualTo(Interpretation.NEUTRAL); + assertThat(infoFG.isSignificant()).isTrue(); + } + + @Test + void getAvailableDimensions() { + assertThat(access.getAvailableDimensions(REPO1_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); + assertThat(access.getAvailableDimensions(REPO2_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "d"), new Dimension("f", "g")); + + Map> dimensions = + access.getAvailableDimensions(List.of(REPO1_ID, REPO2_ID)); + assertThat(dimensions).containsOnlyKeys(REPO1_ID, REPO2_ID); + assertThat(dimensions.get(REPO1_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); + assertThat(dimensions.get(REPO2_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "d"), new Dimension("f", "g")); + + // Change run3's source to a tar source + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl() + .update(RUN) + .set(RUN.COMMIT_HASH, (String) null) + .set(RUN.TAR_DESC, "tarDesc") + .where(RUN.ID.eq(RUN3_ID.getIdAsString())) + .execute(); + } + + assertThat(access.getAvailableDimensions(REPO1_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); + assertThat(access.getAvailableDimensions(REPO2_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "d")); + + dimensions = access.getAvailableDimensions(List.of(REPO1_ID, REPO2_ID)); + assertThat(dimensions).containsOnlyKeys(REPO1_ID, REPO2_ID); + assertThat(dimensions.get(REPO1_ID)) + .containsExactlyInAnyOrder(new Dimension("a", "b"), new Dimension("f", "g")); + assertThat(dimensions.get(REPO2_ID)).containsExactlyInAnyOrder(new Dimension("a", "d")); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfoTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfoTest.java index 7ef7427d6..4ce4b70c6 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfoTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionInfoTest.java @@ -7,53 +7,53 @@ class DimensionInfoTest { - private Dimension dimension; - private Unit unit; - private Interpretation interpretation; - - @BeforeEach - void setUp() { - dimension = new Dimension("test", "dimension"); - unit = new Unit("test unit"); - interpretation = Interpretation.LESS_IS_BETTER; - } - - @Test - void createDimInfo() { - DimensionInfo info = new DimensionInfo(dimension); - assertThat(info.getDimension()).isEqualTo(dimension); - assertThat(info.getUnit()).isEqualTo(Unit.DEFAULT); - assertThat(info.getInterpretation()).isEqualTo(Interpretation.DEFAULT); - assertThat(info.isSignificant()).isTrue(); // Default significance - - info = new DimensionInfo(dimension, unit, interpretation, false); - assertThat(info.getDimension()).isEqualTo(dimension); - assertThat(info.getUnit()).isEqualTo(unit); - assertThat(info.getInterpretation()).isEqualTo(interpretation); - assertThat(info.isSignificant()).isFalse(); - - info = new DimensionInfo(dimension, null, null); - assertThat(info.getDimension()).isEqualTo(dimension); - assertThat(info.getUnit()).isEqualTo(Unit.DEFAULT); - assertThat(info.getInterpretation()).isEqualTo(Interpretation.DEFAULT); - assertThat(info.isSignificant()).isTrue(); // Default significance - - info = new DimensionInfo(dimension, unit, null); - assertThat(info.getDimension()).isEqualTo(dimension); - assertThat(info.getUnit()).isEqualTo(unit); - assertThat(info.getInterpretation()).isEqualTo(Interpretation.DEFAULT); - assertThat(info.isSignificant()).isTrue(); // Default significance - - info = new DimensionInfo(dimension, null, interpretation); - assertThat(info.getDimension()).isEqualTo(dimension); - assertThat(info.getUnit()).isEqualTo(Unit.DEFAULT); - assertThat(info.getInterpretation()).isEqualTo(interpretation); - assertThat(info.isSignificant()).isTrue(); // Default significance - - info = new DimensionInfo(dimension, unit, interpretation); - assertThat(info.getDimension()).isEqualTo(dimension); - assertThat(info.getUnit()).isEqualTo(unit); - assertThat(info.getInterpretation()).isEqualTo(interpretation); - assertThat(info.isSignificant()).isTrue(); // Default significance - } + private Dimension dimension; + private Unit unit; + private Interpretation interpretation; + + @BeforeEach + void setUp() { + dimension = new Dimension("test", "dimension"); + unit = new Unit("test unit"); + interpretation = Interpretation.LESS_IS_BETTER; + } + + @Test + void createDimInfo() { + DimensionInfo info = new DimensionInfo(dimension); + assertThat(info.getDimension()).isEqualTo(dimension); + assertThat(info.getUnit()).isEqualTo(Unit.DEFAULT); + assertThat(info.getInterpretation()).isEqualTo(Interpretation.DEFAULT); + assertThat(info.isSignificant()).isTrue(); // Default significance + + info = new DimensionInfo(dimension, unit, interpretation, false); + assertThat(info.getDimension()).isEqualTo(dimension); + assertThat(info.getUnit()).isEqualTo(unit); + assertThat(info.getInterpretation()).isEqualTo(interpretation); + assertThat(info.isSignificant()).isFalse(); + + info = new DimensionInfo(dimension, null, null); + assertThat(info.getDimension()).isEqualTo(dimension); + assertThat(info.getUnit()).isEqualTo(Unit.DEFAULT); + assertThat(info.getInterpretation()).isEqualTo(Interpretation.DEFAULT); + assertThat(info.isSignificant()).isTrue(); // Default significance + + info = new DimensionInfo(dimension, unit, null); + assertThat(info.getDimension()).isEqualTo(dimension); + assertThat(info.getUnit()).isEqualTo(unit); + assertThat(info.getInterpretation()).isEqualTo(Interpretation.DEFAULT); + assertThat(info.isSignificant()).isTrue(); // Default significance + + info = new DimensionInfo(dimension, null, interpretation); + assertThat(info.getDimension()).isEqualTo(dimension); + assertThat(info.getUnit()).isEqualTo(Unit.DEFAULT); + assertThat(info.getInterpretation()).isEqualTo(interpretation); + assertThat(info.isSignificant()).isTrue(); // Default significance + + info = new DimensionInfo(dimension, unit, interpretation); + assertThat(info.getDimension()).isEqualTo(dimension); + assertThat(info.getUnit()).isEqualTo(unit); + assertThat(info.getInterpretation()).isEqualTo(interpretation); + assertThat(info.isSignificant()).isTrue(); // Default significance + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionTest.java index ec995f2f6..78f11cf19 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/DimensionTest.java @@ -6,32 +6,32 @@ class DimensionTest { - @SuppressWarnings("EqualsWithItself") - @Test - void compareDimensions() { - Dimension dimAA = new Dimension("a", "a"); - Dimension dimAB = new Dimension("a", "b"); - Dimension dimBA = new Dimension("b", "a"); - Dimension dimBB = new Dimension("b", "b"); - - assertThat(dimAA.compareTo(dimAA)).isEqualTo(0); - assertThat(dimAA).isLessThan(dimAB); - assertThat(dimAA).isLessThan(dimBA); - assertThat(dimAA).isLessThan(dimBB); - - assertThat(dimAB).isGreaterThan(dimAA); - assertThat(dimAB.compareTo(dimAB)).isEqualTo(0); - assertThat(dimAB).isLessThan(dimBA); - assertThat(dimAB).isLessThan(dimBB); - - assertThat(dimBA).isGreaterThan(dimAA); - assertThat(dimBA).isGreaterThan(dimAB); - assertThat(dimBA.compareTo(dimBA)).isEqualTo(0); - assertThat(dimBA).isLessThan(dimBB); - - assertThat(dimBB).isGreaterThan(dimAA); - assertThat(dimBB).isGreaterThan(dimAB); - assertThat(dimBB).isGreaterThan(dimBA); - assertThat(dimBB.compareTo(dimBB)).isEqualTo(0); - } + @SuppressWarnings("EqualsWithItself") + @Test + void compareDimensions() { + Dimension dimAA = new Dimension("a", "a"); + Dimension dimAB = new Dimension("a", "b"); + Dimension dimBA = new Dimension("b", "a"); + Dimension dimBB = new Dimension("b", "b"); + + assertThat(dimAA.compareTo(dimAA)).isEqualTo(0); + assertThat(dimAA).isLessThan(dimAB); + assertThat(dimAA).isLessThan(dimBA); + assertThat(dimAA).isLessThan(dimBB); + + assertThat(dimAB).isGreaterThan(dimAA); + assertThat(dimAB.compareTo(dimAB)).isEqualTo(0); + assertThat(dimAB).isLessThan(dimBA); + assertThat(dimAB).isLessThan(dimBB); + + assertThat(dimBA).isGreaterThan(dimAA); + assertThat(dimBA).isGreaterThan(dimAB); + assertThat(dimBA.compareTo(dimBA)).isEqualTo(0); + assertThat(dimBA).isLessThan(dimBB); + + assertThat(dimBB).isGreaterThan(dimAA); + assertThat(dimBB).isGreaterThan(dimAB); + assertThat(dimBB).isGreaterThan(dimBA); + assertThat(dimBB.compareTo(dimBB)).isEqualTo(0); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/InterpretationTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/InterpretationTest.java index c790af834..db68a7f66 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/InterpretationTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/dimensionaccess/entities/InterpretationTest.java @@ -8,38 +8,38 @@ class InterpretationTest { - @Test - void fromTextualRepresentation() { - assertThat(Interpretation.fromTextualRepresentation("LESS_IS_BETTER")) - .isEqualTo(Interpretation.LESS_IS_BETTER); - assertThat(Interpretation.fromTextualRepresentation("MORE_IS_BETTER")) - .isEqualTo(Interpretation.MORE_IS_BETTER); - assertThat(Interpretation.fromTextualRepresentation("NEUTRAL")) - .isEqualTo(Interpretation.NEUTRAL); + @Test + void fromTextualRepresentation() { + assertThat(Interpretation.fromTextualRepresentation("LESS_IS_BETTER")) + .isEqualTo(Interpretation.LESS_IS_BETTER); + assertThat(Interpretation.fromTextualRepresentation("MORE_IS_BETTER")) + .isEqualTo(Interpretation.MORE_IS_BETTER); + assertThat(Interpretation.fromTextualRepresentation("NEUTRAL")) + .isEqualTo(Interpretation.NEUTRAL); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("blubb")) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("less_is_better")) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("LESS_IS_BETTER_")) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("neutral")) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("NEUTRAL ")) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation(" NEUTRAL")) - .isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> Interpretation.fromTextualRepresentation(null)) - .isInstanceOf(IllegalArgumentException.class); - } + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("blubb")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("less_is_better")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("LESS_IS_BETTER_")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("neutral")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation("NEUTRAL ")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation(" NEUTRAL")) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> Interpretation.fromTextualRepresentation(null)) + .isInstanceOf(IllegalArgumentException.class); + } - @Test - void fromSharedRepresentation() { - assertThat(Interpretation.fromSharedRepresentation(Result.Interpretation.LESS_IS_BETTER)) - .isEqualTo(Interpretation.LESS_IS_BETTER); - assertThat(Interpretation.fromSharedRepresentation(Result.Interpretation.MORE_IS_BETTER)) - .isEqualTo(Interpretation.MORE_IS_BETTER); - assertThat(Interpretation.fromSharedRepresentation(Result.Interpretation.NEUTRAL)) - .isEqualTo(Interpretation.NEUTRAL); - } + @Test + void fromSharedRepresentation() { + assertThat(Interpretation.fromSharedRepresentation(Result.Interpretation.LESS_IS_BETTER)) + .isEqualTo(Interpretation.LESS_IS_BETTER); + assertThat(Interpretation.fromSharedRepresentation(Result.Interpretation.MORE_IS_BETTER)) + .isEqualTo(Interpretation.MORE_IS_BETTER); + assertThat(Interpretation.fromSharedRepresentation(Result.Interpretation.NEUTRAL)) + .isEqualTo(Interpretation.NEUTRAL); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccessTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccessTest.java index 25b125822..0a664b630 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccessTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoReadAccessTest.java @@ -25,181 +25,191 @@ class RepoReadAccessTest { - private static final RepoId REPO1_ID = new RepoId(); - private static final String REPO1_NAME = "floob"; - private static final RemoteUrl REPO1_URL = - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"); - private static final List REPO1_BRANCHES = List.of( - new Branch( - REPO1_ID, - BranchName.fromName("main"), - new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"), - true - ), - new Branch( - REPO1_ID, - BranchName.fromName("test"), - new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"), - false - ) - ); - - private static final RepoId REPO2_ID = new RepoId(); - private static final String REPO2_NAME = "bloof"; - private static final RemoteUrl REPO2_URL = - new RemoteUrl("https://github.com/IPDSnelting/comvel.git"); - private static final List REPO2_BRANCHES = List.of( - new Branch( - REPO2_ID, - BranchName.fromName("red"), - new CommitHash("f3f7d9ed781a3a3dd51c7854c0fb4eb1161eab1a"), - true - ), - new Branch( - REPO2_ID, - BranchName.fromName("green"), - new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), - true - ), - new Branch( - REPO2_ID, - BranchName.fromName("blue"), - new CommitHash("0b4f005028fb79d1e8f6c1f6e84dcf804fcf5103"), - false - ) - ); - - private static final RepoId REPO3_ID = new RepoId(); - private static final String REPO3_NAME = "olbof"; - private static final RemoteUrl REPO3_URL = - new RemoteUrl("https://github.com/IPDSnelting/volcem.git"); - private static final List REPO3_BRANCHES = List.of(); - - private RepoReadAccess access; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - TestDb testDb = new TestDb(tempDir); - - // Insert repos - testDb.addRepo(REPO1_ID, REPO1_NAME, REPO1_URL); - testDb.addRepo(REPO2_ID, REPO2_NAME, REPO2_URL); - testDb.addRepo(REPO3_ID, REPO3_NAME, REPO3_URL); - - // Insert dummy commits - Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) - .forEach(branch -> testDb.addCommit(branch.getRepoId(), branch.getLatestCommitHash())); - - // Insert branches - Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) - .forEach(branch -> testDb.addBranch( - branch.getRepoId(), - branch.getName(), - branch.getLatestCommitHash(), - branch.isTracked() - )); - - DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - access = new RepoReadAccess(databaseStorage); - } - - @Test - void getExistingAndNonexistentRepos() { - Repo repo1 = access.getRepo(REPO1_ID); - assertThat(repo1.getId()).isEqualTo(REPO1_ID); - assertThat(repo1.getName()).isEqualTo(REPO1_NAME); - assertThat(repo1.getRemoteUrl()).isEqualTo(REPO1_URL); - - Repo repo2 = access.getRepo(REPO2_ID); - assertThat(repo2.getId()).isEqualTo(REPO2_ID); - assertThat(repo2.getName()).isEqualTo(REPO2_NAME); - assertThat(repo2.getRemoteUrl()).isEqualTo(REPO2_URL); - - Repo repo3 = access.getRepo(REPO3_ID); - assertThat(repo3.getId()).isEqualTo(REPO3_ID); - assertThat(repo3.getName()).isEqualTo(REPO3_NAME); - assertThat(repo3.getRemoteUrl()).isEqualTo(REPO3_URL); - - assertThatCode(() -> access.guardRepoExists(REPO1_ID)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardRepoExists(REPO2_ID)).doesNotThrowAnyException(); - assertThatCode(() -> access.guardRepoExists(REPO3_ID)).doesNotThrowAnyException(); - - RepoId randomId = new RepoId(); - - assertThatThrownBy(() -> access.getRepo(randomId)) - .isInstanceOf(NoSuchRepoException.class) - .extracting("invalidId") - .isEqualTo(randomId); - - assertThatThrownBy(() -> access.guardRepoExists(randomId)) - .isInstanceOf(NoSuchRepoException.class) - .extracting("invalidId") - .isEqualTo(randomId); - } - - @Test - void getAllRepos() { - Map repos = access.getAllRepos().stream() - .collect(toMap( - Repo::getId, - it -> it, - (l, r) -> { - throw new AssertionError("repo ids are unique"); - } - )); - - assertThat(repos).containsOnlyKeys(REPO1_ID, REPO2_ID, REPO3_ID); - - assertThat(repos.get(REPO1_ID).getId()).isEqualTo(REPO1_ID); - assertThat(repos.get(REPO1_ID).getName()).isEqualTo(REPO1_NAME); - assertThat(repos.get(REPO1_ID).getRemoteUrl()).isEqualTo(REPO1_URL); - - assertThat(repos.get(REPO2_ID).getId()).isEqualTo(REPO2_ID); - assertThat(repos.get(REPO2_ID).getName()).isEqualTo(REPO2_NAME); - assertThat(repos.get(REPO2_ID).getRemoteUrl()).isEqualTo(REPO2_URL); - - assertThat(repos.get(REPO3_ID).getId()).isEqualTo(REPO3_ID); - assertThat(repos.get(REPO3_ID).getName()).isEqualTo(REPO3_NAME); - assertThat(repos.get(REPO3_ID).getRemoteUrl()).isEqualTo(REPO3_URL); - } - - @Test - void getAllBranches() { - assertThat(access.getAllBranches(REPO1_ID)) - .containsExactlyInAnyOrderElementsOf(REPO1_BRANCHES); - - assertThat(access.getAllBranches(REPO2_ID)) - .containsExactlyInAnyOrderElementsOf(REPO2_BRANCHES); - - assertThat(access.getAllBranches(REPO3_ID)) - .containsExactlyInAnyOrderElementsOf(REPO3_BRANCHES); - - assertThat(access.getAllBranches(new RepoId())) - .isEmpty(); - } - - @Test - void searchBranches() { - assertThat(access.searchBranches(5, null, "n")) - .containsExactlyInAnyOrder( - new SearchBranchDescription(REPO1_ID, BranchName.fromName("main"), - new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"), "message", false), - new SearchBranchDescription(REPO2_ID, BranchName.fromName("green"), - new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), "message", false) - ); - - assertThat(access.searchBranches(5, REPO1_ID, "e")) - .containsExactlyInAnyOrder( - new SearchBranchDescription(REPO1_ID, BranchName.fromName("test"), - new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"), "message", false) - ); - - assertThat(access.searchBranches(2, null, "e")) - .containsExactly( - new SearchBranchDescription(REPO2_ID, BranchName.fromName("blue"), - new CommitHash("0b4f005028fb79d1e8f6c1f6e84dcf804fcf5103"), "message", false), - new SearchBranchDescription(REPO2_ID, BranchName.fromName("green"), - new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), "message", false) - ); - } + private static final RepoId REPO1_ID = new RepoId(); + private static final String REPO1_NAME = "floob"; + private static final RemoteUrl REPO1_URL = + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"); + private static final List REPO1_BRANCHES = + List.of( + new Branch( + REPO1_ID, + BranchName.fromName("main"), + new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"), + true), + new Branch( + REPO1_ID, + BranchName.fromName("test"), + new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"), + false)); + + private static final RepoId REPO2_ID = new RepoId(); + private static final String REPO2_NAME = "bloof"; + private static final RemoteUrl REPO2_URL = + new RemoteUrl("https://github.com/IPDSnelting/comvel.git"); + private static final List REPO2_BRANCHES = + List.of( + new Branch( + REPO2_ID, + BranchName.fromName("red"), + new CommitHash("f3f7d9ed781a3a3dd51c7854c0fb4eb1161eab1a"), + true), + new Branch( + REPO2_ID, + BranchName.fromName("green"), + new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), + true), + new Branch( + REPO2_ID, + BranchName.fromName("blue"), + new CommitHash("0b4f005028fb79d1e8f6c1f6e84dcf804fcf5103"), + false)); + + private static final RepoId REPO3_ID = new RepoId(); + private static final String REPO3_NAME = "olbof"; + private static final RemoteUrl REPO3_URL = + new RemoteUrl("https://github.com/IPDSnelting/volcem.git"); + private static final List REPO3_BRANCHES = List.of(); + + private RepoReadAccess access; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + TestDb testDb = new TestDb(tempDir); + + // Insert repos + testDb.addRepo(REPO1_ID, REPO1_NAME, REPO1_URL); + testDb.addRepo(REPO2_ID, REPO2_NAME, REPO2_URL); + testDb.addRepo(REPO3_ID, REPO3_NAME, REPO3_URL); + + // Insert dummy commits + Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) + .forEach(branch -> testDb.addCommit(branch.getRepoId(), branch.getLatestCommitHash())); + + // Insert branches + Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) + .forEach( + branch -> + testDb.addBranch( + branch.getRepoId(), + branch.getName(), + branch.getLatestCommitHash(), + branch.isTracked())); + + DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + access = new RepoReadAccess(databaseStorage); + } + + @Test + void getExistingAndNonexistentRepos() { + Repo repo1 = access.getRepo(REPO1_ID); + assertThat(repo1.getId()).isEqualTo(REPO1_ID); + assertThat(repo1.getName()).isEqualTo(REPO1_NAME); + assertThat(repo1.getRemoteUrl()).isEqualTo(REPO1_URL); + + Repo repo2 = access.getRepo(REPO2_ID); + assertThat(repo2.getId()).isEqualTo(REPO2_ID); + assertThat(repo2.getName()).isEqualTo(REPO2_NAME); + assertThat(repo2.getRemoteUrl()).isEqualTo(REPO2_URL); + + Repo repo3 = access.getRepo(REPO3_ID); + assertThat(repo3.getId()).isEqualTo(REPO3_ID); + assertThat(repo3.getName()).isEqualTo(REPO3_NAME); + assertThat(repo3.getRemoteUrl()).isEqualTo(REPO3_URL); + + assertThatCode(() -> access.guardRepoExists(REPO1_ID)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardRepoExists(REPO2_ID)).doesNotThrowAnyException(); + assertThatCode(() -> access.guardRepoExists(REPO3_ID)).doesNotThrowAnyException(); + + RepoId randomId = new RepoId(); + + assertThatThrownBy(() -> access.getRepo(randomId)) + .isInstanceOf(NoSuchRepoException.class) + .extracting("invalidId") + .isEqualTo(randomId); + + assertThatThrownBy(() -> access.guardRepoExists(randomId)) + .isInstanceOf(NoSuchRepoException.class) + .extracting("invalidId") + .isEqualTo(randomId); + } + + @Test + void getAllRepos() { + Map repos = + access.getAllRepos().stream() + .collect( + toMap( + Repo::getId, + it -> it, + (l, r) -> { + throw new AssertionError("repo ids are unique"); + })); + + assertThat(repos).containsOnlyKeys(REPO1_ID, REPO2_ID, REPO3_ID); + + assertThat(repos.get(REPO1_ID).getId()).isEqualTo(REPO1_ID); + assertThat(repos.get(REPO1_ID).getName()).isEqualTo(REPO1_NAME); + assertThat(repos.get(REPO1_ID).getRemoteUrl()).isEqualTo(REPO1_URL); + + assertThat(repos.get(REPO2_ID).getId()).isEqualTo(REPO2_ID); + assertThat(repos.get(REPO2_ID).getName()).isEqualTo(REPO2_NAME); + assertThat(repos.get(REPO2_ID).getRemoteUrl()).isEqualTo(REPO2_URL); + + assertThat(repos.get(REPO3_ID).getId()).isEqualTo(REPO3_ID); + assertThat(repos.get(REPO3_ID).getName()).isEqualTo(REPO3_NAME); + assertThat(repos.get(REPO3_ID).getRemoteUrl()).isEqualTo(REPO3_URL); + } + + @Test + void getAllBranches() { + assertThat(access.getAllBranches(REPO1_ID)).containsExactlyInAnyOrderElementsOf(REPO1_BRANCHES); + + assertThat(access.getAllBranches(REPO2_ID)).containsExactlyInAnyOrderElementsOf(REPO2_BRANCHES); + + assertThat(access.getAllBranches(REPO3_ID)).containsExactlyInAnyOrderElementsOf(REPO3_BRANCHES); + + assertThat(access.getAllBranches(new RepoId())).isEmpty(); + } + + @Test + void searchBranches() { + assertThat(access.searchBranches(5, null, "n")) + .containsExactlyInAnyOrder( + new SearchBranchDescription( + REPO1_ID, + BranchName.fromName("main"), + new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"), + "message", + false), + new SearchBranchDescription( + REPO2_ID, + BranchName.fromName("green"), + new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), + "message", + false)); + + assertThat(access.searchBranches(5, REPO1_ID, "e")) + .containsExactlyInAnyOrder( + new SearchBranchDescription( + REPO1_ID, + BranchName.fromName("test"), + new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"), + "message", + false)); + + assertThat(access.searchBranches(2, null, "e")) + .containsExactly( + new SearchBranchDescription( + REPO2_ID, + BranchName.fromName("blue"), + new CommitHash("0b4f005028fb79d1e8f6c1f6e84dcf804fcf5103"), + "message", + false), + new SearchBranchDescription( + REPO2_ID, + BranchName.fromName("green"), + new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), + "message", + false)); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccessTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccessTest.java index 070e7d75a..3af346a24 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccessTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/RepoWriteAccessTest.java @@ -33,198 +33,192 @@ class RepoWriteAccessTest { - private static final RepoId REPO1_ID = new RepoId(); - private static final String REPO1_NAME = "floob"; - private static final RemoteUrl REPO1_URL = - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"); - private static final List REPO1_BRANCHES = List.of( - new Branch( - REPO1_ID, - BranchName.fromName("main"), - new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"), - true - ), - new Branch( - REPO1_ID, - BranchName.fromName("test"), - new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"), - false - ) - ); - - private static final RepoId REPO2_ID = new RepoId(); - private static final String REPO2_NAME = "bloof"; - private static final RemoteUrl REPO2_URL = - new RemoteUrl("https://github.com/IPDSnelting/comvel.git"); - private static final List REPO2_BRANCHES = List.of( - new Branch( - REPO2_ID, - BranchName.fromName("main"), - new CommitHash("f3f7d9ed781a3a3dd51c7854c0fb4eb1161eab1a"), - true - ), - new Branch( - REPO2_ID, - BranchName.fromName("test"), - new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), - false - ) - ); - - private static final RepoId REPO3_ID = new RepoId(); - private static final String REPO3_NAME = "olbof"; - private static final RemoteUrl REPO3_URL = - new RemoteUrl("https://github.com/IPDSnelting/volcem.git"); - private static final List REPO3_BRANCHES = List.of(); - - private DatabaseStorage databaseStorage; - private AvailableDimensionsCache availableDimensionsCache; - private RunCache runCache; - private LatestRunCache latestRunCache; - private RepoWriteAccess access; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - TestDb testDb = new TestDb(tempDir); - - // Insert repos - testDb.addRepo(REPO1_ID, REPO1_NAME, REPO1_URL); - testDb.addRepo(REPO2_ID, REPO2_NAME, REPO2_URL); - testDb.addRepo(REPO3_ID, REPO3_NAME, REPO3_URL); - - // Insert dummy commits - Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) - .forEach(branch -> testDb.addCommit(branch.getRepoId(), branch.getLatestCommitHash())); - - // Insert branches - Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) - .forEach(branch -> testDb.addBranch( - branch.getRepoId(), - branch.getName(), - branch.getLatestCommitHash(), - branch.isTracked() - )); - - databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - availableDimensionsCache = mock(AvailableDimensionsCache.class); - runCache = mock(RunCache.class); - latestRunCache = mock(LatestRunCache.class); - access = new RepoWriteAccess(databaseStorage, availableDimensionsCache, runCache, - latestRunCache); - } - - @Test - void addRepo() { - Repo newRepo = access.addRepo("new repo", new RemoteUrl("https://foo.bar/xyz.git")); - - assertThat(newRepo.getName()).isEqualTo("new repo"); - assertThat(newRepo.getRemoteUrl()).isEqualTo(new RemoteUrl("https://foo.bar/xyz.git")); - - assertThat(access.getAllRepos().stream() - .map(Repo::getId) - .collect(toList())) - .containsExactlyInAnyOrder(REPO1_ID, REPO2_ID, REPO3_ID, newRepo.getId()); - - Repo newRepo2 = access.getRepo(newRepo.getId()); - - assertThat(newRepo2.getId()).isEqualTo(newRepo.getId()); - assertThat(newRepo2.getName()).isEqualTo(newRepo.getName()); - assertThat(newRepo2.getRemoteUrl()).isEqualTo(newRepo.getRemoteUrl()); - } - - @Test - void deleteRepo() { - access.deleteRepo(REPO2_ID); - - verify(availableDimensionsCache, atLeastOnce()).invalidate(REPO2_ID); - verify(runCache, atLeastOnce()).invalidateAll(); - verify(latestRunCache, atLeastOnce()).invalidate(REPO2_ID); - - assertThat(access.getAllRepos().stream().map(Repo::getId)) - .containsExactlyInAnyOrder(REPO1_ID, REPO3_ID); - - assertThatThrownBy(() -> access.getRepo(REPO2_ID)) - .isInstanceOf(NoSuchRepoException.class) - .extracting("invalidId") - .isEqualTo(REPO2_ID); - - assertThat(access.getAllBranches(REPO2_ID)).isEmpty(); - - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - Result repo2Commits = db.dsl() - .selectFrom(KNOWN_COMMIT) - .where(KNOWN_COMMIT.REPO_ID.eq(REPO2_ID.getIdAsString())) - .fetch(); - - assertThat(repo2Commits).isEmpty(); - } - } - - @Test - void updateBranches() { - access.setTrackedBranches(REPO1_ID, Set.of(BranchName.fromName("test"))); - - List repo1Branches = access.getAllBranches(REPO1_ID); - assertThat(repo1Branches.stream() - .map(Branch::getName) - .map(BranchName::getName)) - .containsExactlyInAnyOrder("main", "test"); - assertThat(repo1Branches.stream() - .filter(Branch::isTracked) - .map(Branch::getName) - .map(BranchName::getName)) - .containsExactlyInAnyOrder("test"); - - // Has it modified other branches on accident? - - List repo2Branches = access.getAllBranches(REPO2_ID); - assertThat(repo2Branches.stream() - .map(Branch::getName) - .map(BranchName::getName)) - .containsExactlyInAnyOrder("main", "test"); - assertThat(repo2Branches.stream() - .filter(Branch::isTracked) - .map(Branch::getName) - .map(BranchName::getName)) - .containsExactlyInAnyOrder("main"); - - assertThat(access.getAllBranches(REPO3_ID)).isEmpty(); - } - - @Test - void updateRepo() { - // Setting nothing changes nothing - access.updateRepo(REPO1_ID, null, null); - Repo repo1 = access.getRepo(REPO1_ID); - assertThat(repo1.getName()).isEqualTo(REPO1_NAME); - assertThat(repo1.getRemoteUrl()).isEqualTo(REPO1_URL); - - // Setting name - access.updateRepo(REPO1_ID, "new name", null); - repo1 = access.getRepo(REPO1_ID); - assertThat(repo1.getName()).isEqualTo("new name"); - assertThat(repo1.getRemoteUrl()).isEqualTo(REPO1_URL); - - // Setting remote url - access.updateRepo(REPO1_ID, null, new RemoteUrl("https://flab.argle/")); - repo1 = access.getRepo(REPO1_ID); - assertThat(repo1.getName()).isEqualTo("new name"); - assertThat(repo1.getRemoteUrl()).isEqualTo(new RemoteUrl("https://flab.argle/")); - - // Setting both at the same time - access.updateRepo(REPO1_ID, "even newer name", new RemoteUrl("https://bong.le")); - repo1 = access.getRepo(REPO1_ID); - assertThat(repo1.getName()).isEqualTo("even newer name"); - assertThat(repo1.getRemoteUrl()).isEqualTo(new RemoteUrl("https://bong.le")); - - // Other repos have not been changed - - Repo repo2 = access.getRepo(REPO2_ID); - assertThat(repo2.getName()).isEqualTo(REPO2_NAME); - assertThat(repo2.getRemoteUrl()).isEqualTo(REPO2_URL); - - Repo repo3 = access.getRepo(REPO3_ID); - assertThat(repo3.getName()).isEqualTo(REPO3_NAME); - assertThat(repo3.getRemoteUrl()).isEqualTo(REPO3_URL); - } + private static final RepoId REPO1_ID = new RepoId(); + private static final String REPO1_NAME = "floob"; + private static final RemoteUrl REPO1_URL = + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"); + private static final List REPO1_BRANCHES = + List.of( + new Branch( + REPO1_ID, + BranchName.fromName("main"), + new CommitHash("07dcbfbee1c5c833614d00ce70c15621c939806c"), + true), + new Branch( + REPO1_ID, + BranchName.fromName("test"), + new CommitHash("57b0e77894a2b7270ade8767b355ed8a283fffb0"), + false)); + + private static final RepoId REPO2_ID = new RepoId(); + private static final String REPO2_NAME = "bloof"; + private static final RemoteUrl REPO2_URL = + new RemoteUrl("https://github.com/IPDSnelting/comvel.git"); + private static final List REPO2_BRANCHES = + List.of( + new Branch( + REPO2_ID, + BranchName.fromName("main"), + new CommitHash("f3f7d9ed781a3a3dd51c7854c0fb4eb1161eab1a"), + true), + new Branch( + REPO2_ID, + BranchName.fromName("test"), + new CommitHash("018ec13f2c2d2a711c44a4f8b7ff0f29050e6234"), + false)); + + private static final RepoId REPO3_ID = new RepoId(); + private static final String REPO3_NAME = "olbof"; + private static final RemoteUrl REPO3_URL = + new RemoteUrl("https://github.com/IPDSnelting/volcem.git"); + private static final List REPO3_BRANCHES = List.of(); + + private DatabaseStorage databaseStorage; + private AvailableDimensionsCache availableDimensionsCache; + private RunCache runCache; + private LatestRunCache latestRunCache; + private RepoWriteAccess access; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + TestDb testDb = new TestDb(tempDir); + + // Insert repos + testDb.addRepo(REPO1_ID, REPO1_NAME, REPO1_URL); + testDb.addRepo(REPO2_ID, REPO2_NAME, REPO2_URL); + testDb.addRepo(REPO3_ID, REPO3_NAME, REPO3_URL); + + // Insert dummy commits + Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) + .forEach(branch -> testDb.addCommit(branch.getRepoId(), branch.getLatestCommitHash())); + + // Insert branches + Stream.concat(REPO1_BRANCHES.stream(), REPO2_BRANCHES.stream()) + .forEach( + branch -> + testDb.addBranch( + branch.getRepoId(), + branch.getName(), + branch.getLatestCommitHash(), + branch.isTracked())); + + databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + availableDimensionsCache = mock(AvailableDimensionsCache.class); + runCache = mock(RunCache.class); + latestRunCache = mock(LatestRunCache.class); + access = + new RepoWriteAccess(databaseStorage, availableDimensionsCache, runCache, latestRunCache); + } + + @Test + void addRepo() { + Repo newRepo = access.addRepo("new repo", new RemoteUrl("https://foo.bar/xyz.git")); + + assertThat(newRepo.getName()).isEqualTo("new repo"); + assertThat(newRepo.getRemoteUrl()).isEqualTo(new RemoteUrl("https://foo.bar/xyz.git")); + + assertThat(access.getAllRepos().stream().map(Repo::getId).collect(toList())) + .containsExactlyInAnyOrder(REPO1_ID, REPO2_ID, REPO3_ID, newRepo.getId()); + + Repo newRepo2 = access.getRepo(newRepo.getId()); + + assertThat(newRepo2.getId()).isEqualTo(newRepo.getId()); + assertThat(newRepo2.getName()).isEqualTo(newRepo.getName()); + assertThat(newRepo2.getRemoteUrl()).isEqualTo(newRepo.getRemoteUrl()); + } + + @Test + void deleteRepo() { + access.deleteRepo(REPO2_ID); + + verify(availableDimensionsCache, atLeastOnce()).invalidate(REPO2_ID); + verify(runCache, atLeastOnce()).invalidateAll(); + verify(latestRunCache, atLeastOnce()).invalidate(REPO2_ID); + + assertThat(access.getAllRepos().stream().map(Repo::getId)) + .containsExactlyInAnyOrder(REPO1_ID, REPO3_ID); + + assertThatThrownBy(() -> access.getRepo(REPO2_ID)) + .isInstanceOf(NoSuchRepoException.class) + .extracting("invalidId") + .isEqualTo(REPO2_ID); + + assertThat(access.getAllBranches(REPO2_ID)).isEmpty(); + + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + Result repo2Commits = + db.dsl() + .selectFrom(KNOWN_COMMIT) + .where(KNOWN_COMMIT.REPO_ID.eq(REPO2_ID.getIdAsString())) + .fetch(); + + assertThat(repo2Commits).isEmpty(); + } + } + + @Test + void updateBranches() { + access.setTrackedBranches(REPO1_ID, Set.of(BranchName.fromName("test"))); + + List repo1Branches = access.getAllBranches(REPO1_ID); + assertThat(repo1Branches.stream().map(Branch::getName).map(BranchName::getName)) + .containsExactlyInAnyOrder("main", "test"); + assertThat( + repo1Branches.stream() + .filter(Branch::isTracked) + .map(Branch::getName) + .map(BranchName::getName)) + .containsExactlyInAnyOrder("test"); + + // Has it modified other branches on accident? + + List repo2Branches = access.getAllBranches(REPO2_ID); + assertThat(repo2Branches.stream().map(Branch::getName).map(BranchName::getName)) + .containsExactlyInAnyOrder("main", "test"); + assertThat( + repo2Branches.stream() + .filter(Branch::isTracked) + .map(Branch::getName) + .map(BranchName::getName)) + .containsExactlyInAnyOrder("main"); + + assertThat(access.getAllBranches(REPO3_ID)).isEmpty(); + } + + @Test + void updateRepo() { + // Setting nothing changes nothing + access.updateRepo(REPO1_ID, null, null); + Repo repo1 = access.getRepo(REPO1_ID); + assertThat(repo1.getName()).isEqualTo(REPO1_NAME); + assertThat(repo1.getRemoteUrl()).isEqualTo(REPO1_URL); + + // Setting name + access.updateRepo(REPO1_ID, "new name", null); + repo1 = access.getRepo(REPO1_ID); + assertThat(repo1.getName()).isEqualTo("new name"); + assertThat(repo1.getRemoteUrl()).isEqualTo(REPO1_URL); + + // Setting remote url + access.updateRepo(REPO1_ID, null, new RemoteUrl("https://flab.argle/")); + repo1 = access.getRepo(REPO1_ID); + assertThat(repo1.getName()).isEqualTo("new name"); + assertThat(repo1.getRemoteUrl()).isEqualTo(new RemoteUrl("https://flab.argle/")); + + // Setting both at the same time + access.updateRepo(REPO1_ID, "even newer name", new RemoteUrl("https://bong.le")); + repo1 = access.getRepo(REPO1_ID); + assertThat(repo1.getName()).isEqualTo("even newer name"); + assertThat(repo1.getRemoteUrl()).isEqualTo(new RemoteUrl("https://bong.le")); + + // Other repos have not been changed + + Repo repo2 = access.getRepo(REPO2_ID); + assertThat(repo2.getName()).isEqualTo(REPO2_NAME); + assertThat(repo2.getRemoteUrl()).isEqualTo(REPO2_URL); + + Repo repo3 = access.getRepo(REPO3_ID); + assertThat(repo3.getName()).isEqualTo(REPO3_NAME); + assertThat(repo3.getRemoteUrl()).isEqualTo(REPO3_URL); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoTest.java index 589ee3d3f..e6224be02 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/access/repoaccess/entities/RepoTest.java @@ -10,70 +10,89 @@ class RepoTest { - @Test - void testIllegalConstructorArguments() { - Assertions.assertThrows(IllegalArgumentException.class, () -> new Repo( - new RepoId(), - "velcom", - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), - null, - Instant.now() - )); + @Test + void testIllegalConstructorArguments() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + new Repo( + new RepoId(), + "velcom", + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), + null, + Instant.now())); - Assertions.assertThrows(IllegalArgumentException.class, () -> new Repo( - new RepoId(), - "velcom", - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), - "bla", - null - )); - } + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + new Repo( + new RepoId(), + "velcom", + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), + "bla", + null)); + } - @Test - void testGhRepoName() { - Repo repo = new Repo(new RepoId(), "velcom via https", - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), null, null); - assertThat(repo.getGithubRepoName()).isEqualTo(Optional.of("IPDSnelting/velcom")); + @Test + void testGhRepoName() { + Repo repo = + new Repo( + new RepoId(), + "velcom via https", + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), + null, + null); + assertThat(repo.getGithubRepoName()).isEqualTo(Optional.of("IPDSnelting/velcom")); - repo = new Repo(new RepoId(), "velcom via ssh", - new RemoteUrl("git@github.com:IPDSnelting/velcom.git"), null, null); - assertThat(repo.getGithubRepoName()).isEqualTo(Optional.of("IPDSnelting/velcom")); + repo = + new Repo( + new RepoId(), + "velcom via ssh", + new RemoteUrl("git@github.com:IPDSnelting/velcom.git"), + null, + null); + assertThat(repo.getGithubRepoName()).isEqualTo(Optional.of("IPDSnelting/velcom")); - repo = new Repo(new RepoId(), "velcom via gitlab(!?)", - new RemoteUrl("https://gitlab.com/IPDSnelting/velcom.git"), null, null); - assertThat(repo.getGithubRepoName()).isEmpty(); - } + repo = + new Repo( + new RepoId(), + "velcom via gitlab(!?)", + new RemoteUrl("https://gitlab.com/IPDSnelting/velcom.git"), + null, + null); + assertThat(repo.getGithubRepoName()).isEmpty(); + } - @Test - void testGhInfo() { - Instant now = Instant.now(); + @Test + void testGhInfo() { + Instant now = Instant.now(); - Repo repo = new Repo( - new RepoId(), - "velcom", - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), - null, - null - ); - assertThat(repo.getGithubInfo()).isEmpty(); + Repo repo = + new Repo( + new RepoId(), + "velcom", + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), + null, + null); + assertThat(repo.getGithubInfo()).isEmpty(); - repo = new Repo( - new RepoId(), - "velcom", - new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), - "token", - now - ); - assertThat(repo.getGithubInfo()) - .isEqualTo(Optional.of(new GithubInfo("IPDSnelting/velcom", "token", now))); + repo = + new Repo( + new RepoId(), + "velcom", + new RemoteUrl("https://github.com/IPDSnelting/velcom.git"), + "token", + now); + assertThat(repo.getGithubInfo()) + .isEqualTo(Optional.of(new GithubInfo("IPDSnelting/velcom", "token", now))); - repo = new Repo( - new RepoId(), - "velcom via https", - new RemoteUrl("https://gitlab.com/IPDSnelting/velcom.git"), - "token", - now - ); - assertThat(repo.getGithubInfo()).isEmpty(); - } + repo = + new Repo( + new RepoId(), + "velcom via https", + new RemoteUrl("https://gitlab.com/IPDSnelting/velcom.git"), + "token", + now); + assertThat(repo.getGithubInfo()).isEmpty(); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/data/queue/PolicyTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/data/queue/PolicyTest.java index ffa188a88..daa6fe6cf 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/data/queue/PolicyTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/data/queue/PolicyTest.java @@ -19,157 +19,157 @@ class PolicyTest { - private final String description = "test description"; - private final CommitHash hash = new CommitHash("a2ec9e64ca2a4243a15554a2678e6af95ce97b7a"); - - private RepoId repo1; - private RepoId repo2; - private RepoId repo3; - private RepoId repo4; - - @BeforeEach - void setUp() { - repo1 = new RepoId(UUID.fromString("2ea6e24a-ef7e-4cac-bb08-054b0e24473a")); - repo2 = new RepoId(UUID.fromString("6bed466a-0a48-4f14-a708-3ac8c02abd72")); - repo3 = new RepoId(UUID.fromString("8b8a568b-24e4-433e-9c6c-ec97a63d84ed")); - repo4 = new RepoId(UUID.fromString("cfa4de7c-8fb2-4f80-8998-94c81586b756")); - } - - private Task comTask(String author, TaskPriority priority, RepoId repoId) { - return new Task(author, priority, Either.ofLeft(new CommitSource(repoId, hash))); - } - - private Task tarTask(String author, TaskPriority priority, @Nullable RepoId repoId) { - return new Task(author, priority, Either.ofRight(new TarSource(description, repoId))); - } - - // Use the policy's built-in function for getting all tasks in order - private List useStepAll(List tasks, @Nullable RepoId repoId) { - return new Policy(tasks, repoId).stepAll(); - } - - // Try to simulate starting tasks one by one via the Queue - private List simulQueue(List tasks, @Nullable RepoId repoId) { - List currentTasks = new ArrayList<>(tasks); - Optional currentRepoId = Optional.ofNullable(repoId); - - List result = new ArrayList<>(); - - while (true) { - Policy policy = new Policy(currentTasks, currentRepoId.orElse(null)); - Optional task = policy.step(); - - if (task.isPresent()) { - result.add(task.get()); - currentTasks.removeIf(t -> t.getId().equals(task.get().getId())); - currentRepoId = policy.getCurrentRepoId(); - } else { - break; - } - } - - return result; - } - - @Test - void manualTasksInFiloOrder() { - Task task1 = comTask("task1", TaskPriority.MANUAL, repo1); - Task task2 = tarTask("task2", TaskPriority.MANUAL, null); - Task task3 = comTask("task3", TaskPriority.MANUAL, repo2); - Task task4 = tarTask("task4", TaskPriority.MANUAL, repo1); - - List tasks = List.of(task3, task2, task4, task1); - List expected = List.of(task4, task3, task2, task1); - - assertThat(useStepAll(tasks, null)).isEqualTo(expected); - assertThat(simulQueue(tasks, null)).isEqualTo(expected); - } - - @Test - void tarTasksInFifoOrder() { - Task task1 = tarTask("task1", TaskPriority.USER_CREATED, repo1); - Task task2 = tarTask("task2", TaskPriority.USER_CREATED, null); - Task task3 = tarTask("task3", TaskPriority.USER_CREATED, repo2); - Task task4 = tarTask("task4", TaskPriority.USER_CREATED, repo1); - - List tasks = List.of(task3, task2, task4, task1); - List expected = List.of(task1, task2, task3, task4); - - assertThat(useStepAll(tasks, null)).isEqualTo(expected); - assertThat(simulQueue(tasks, null)).isEqualTo(expected); - } - - @Test - void listenerTasksInFiloOrder() { - Task task1 = comTask("task1", TaskPriority.LISTENER, repo1); - Task task2 = comTask("task2", TaskPriority.LISTENER, repo1); - Task task3 = comTask("task3", TaskPriority.LISTENER, repo1); - Task task4 = comTask("task4", TaskPriority.LISTENER, repo1); - - List tasks = List.of(task3, task2, task4, task1); - List expected = List.of(task4, task3, task2, task1); - - assertThat(useStepAll(tasks, null)).isEqualTo(expected); - assertThat(simulQueue(tasks, null)).isEqualTo(expected); - } - - @Test - void listenerTasksRoundRobinAndFilo() { - Task r1t1 = comTask("r1t1", TaskPriority.LISTENER, repo1); - Task r1t2 = comTask("r1t2", TaskPriority.LISTENER, repo1); - Task r1t3 = comTask("r1t3", TaskPriority.LISTENER, repo1); - Task r1t4 = comTask("r1t4", TaskPriority.LISTENER, repo1); - - Task r2t1 = comTask("r2t1", TaskPriority.LISTENER, repo2); - - Task r3t1 = comTask("r3t1", TaskPriority.LISTENER, repo3); - Task r3t2 = comTask("r3t2", TaskPriority.LISTENER, repo3); - Task r3t3 = comTask("r3t3", TaskPriority.LISTENER, repo3); - - List tasks = List.of(r1t3, r3t2, r2t1, r1t1, r1t2, r3t3, r3t1, r1t4); - List expected = List.of(r1t4, r2t1, r3t3, r1t3, r3t2, r1t2, r3t1, r1t1); - - assertThat(useStepAll(tasks, repo1)).isEqualTo(expected); - assertThat(simulQueue(tasks, repo1)).isEqualTo(expected); - } - - @Test - void manualBeforeTarBeforeListener() { - Task m1 = comTask("m1", TaskPriority.MANUAL, repo1); - Task m2 = tarTask("m2", TaskPriority.MANUAL, null); - - Task t1 = tarTask("t1", TaskPriority.USER_CREATED, repo2); - Task t2 = tarTask("t2", TaskPriority.USER_CREATED, null); - - Task l1 = comTask("l1", TaskPriority.LISTENER, repo1); - Task l2 = comTask("l2", TaskPriority.LISTENER, repo2); - Task l3 = comTask("l3", TaskPriority.LISTENER, repo2); - - List tasks = List.of(l2, t1, t2, m2, l3, m1, l1); - List expected = List.of(m2, m1, t1, t2, l1, l3, l2); - - assertThat(useStepAll(tasks, repo1)).isEqualTo(expected); - assertThat(simulQueue(tasks, repo1)).isEqualTo(expected); - } - - @Test - void findCorrectNextRepo() { - Task r1 = comTask("r1", TaskPriority.LISTENER, repo1); - Task r2 = comTask("r2", TaskPriority.LISTENER, repo2); - Task r4 = comTask("r4", TaskPriority.LISTENER, repo4); - - List tasks = List.of(r4, r2, r1); - - List expected1 = List.of(r1, r2, r4); - assertThat(useStepAll(tasks, null)).isEqualTo(expected1); - assertThat(simulQueue(tasks, null)).isEqualTo(expected1); - - List expected2 = List.of(r2, r4, r1); - assertThat(useStepAll(tasks, repo2)).isEqualTo(expected2); - assertThat(simulQueue(tasks, repo2)).isEqualTo(expected2); - - List expected3 = List.of(r4, r1, r2); - assertThat(useStepAll(tasks, repo3)).isEqualTo(expected3); - assertThat(simulQueue(tasks, repo3)).isEqualTo(expected3); - } + private final String description = "test description"; + private final CommitHash hash = new CommitHash("a2ec9e64ca2a4243a15554a2678e6af95ce97b7a"); + + private RepoId repo1; + private RepoId repo2; + private RepoId repo3; + private RepoId repo4; + + @BeforeEach + void setUp() { + repo1 = new RepoId(UUID.fromString("2ea6e24a-ef7e-4cac-bb08-054b0e24473a")); + repo2 = new RepoId(UUID.fromString("6bed466a-0a48-4f14-a708-3ac8c02abd72")); + repo3 = new RepoId(UUID.fromString("8b8a568b-24e4-433e-9c6c-ec97a63d84ed")); + repo4 = new RepoId(UUID.fromString("cfa4de7c-8fb2-4f80-8998-94c81586b756")); + } + + private Task comTask(String author, TaskPriority priority, RepoId repoId) { + return new Task(author, priority, Either.ofLeft(new CommitSource(repoId, hash))); + } + + private Task tarTask(String author, TaskPriority priority, @Nullable RepoId repoId) { + return new Task(author, priority, Either.ofRight(new TarSource(description, repoId))); + } + + // Use the policy's built-in function for getting all tasks in order + private List useStepAll(List tasks, @Nullable RepoId repoId) { + return new Policy(tasks, repoId).stepAll(); + } + + // Try to simulate starting tasks one by one via the Queue + private List simulQueue(List tasks, @Nullable RepoId repoId) { + List currentTasks = new ArrayList<>(tasks); + Optional currentRepoId = Optional.ofNullable(repoId); + + List result = new ArrayList<>(); + + while (true) { + Policy policy = new Policy(currentTasks, currentRepoId.orElse(null)); + Optional task = policy.step(); + + if (task.isPresent()) { + result.add(task.get()); + currentTasks.removeIf(t -> t.getId().equals(task.get().getId())); + currentRepoId = policy.getCurrentRepoId(); + } else { + break; + } + } + + return result; + } + + @Test + void manualTasksInFiloOrder() { + Task task1 = comTask("task1", TaskPriority.MANUAL, repo1); + Task task2 = tarTask("task2", TaskPriority.MANUAL, null); + Task task3 = comTask("task3", TaskPriority.MANUAL, repo2); + Task task4 = tarTask("task4", TaskPriority.MANUAL, repo1); + + List tasks = List.of(task3, task2, task4, task1); + List expected = List.of(task4, task3, task2, task1); + + assertThat(useStepAll(tasks, null)).isEqualTo(expected); + assertThat(simulQueue(tasks, null)).isEqualTo(expected); + } + + @Test + void tarTasksInFifoOrder() { + Task task1 = tarTask("task1", TaskPriority.USER_CREATED, repo1); + Task task2 = tarTask("task2", TaskPriority.USER_CREATED, null); + Task task3 = tarTask("task3", TaskPriority.USER_CREATED, repo2); + Task task4 = tarTask("task4", TaskPriority.USER_CREATED, repo1); + + List tasks = List.of(task3, task2, task4, task1); + List expected = List.of(task1, task2, task3, task4); + + assertThat(useStepAll(tasks, null)).isEqualTo(expected); + assertThat(simulQueue(tasks, null)).isEqualTo(expected); + } + + @Test + void listenerTasksInFiloOrder() { + Task task1 = comTask("task1", TaskPriority.LISTENER, repo1); + Task task2 = comTask("task2", TaskPriority.LISTENER, repo1); + Task task3 = comTask("task3", TaskPriority.LISTENER, repo1); + Task task4 = comTask("task4", TaskPriority.LISTENER, repo1); + + List tasks = List.of(task3, task2, task4, task1); + List expected = List.of(task4, task3, task2, task1); + + assertThat(useStepAll(tasks, null)).isEqualTo(expected); + assertThat(simulQueue(tasks, null)).isEqualTo(expected); + } + + @Test + void listenerTasksRoundRobinAndFilo() { + Task r1t1 = comTask("r1t1", TaskPriority.LISTENER, repo1); + Task r1t2 = comTask("r1t2", TaskPriority.LISTENER, repo1); + Task r1t3 = comTask("r1t3", TaskPriority.LISTENER, repo1); + Task r1t4 = comTask("r1t4", TaskPriority.LISTENER, repo1); + + Task r2t1 = comTask("r2t1", TaskPriority.LISTENER, repo2); + + Task r3t1 = comTask("r3t1", TaskPriority.LISTENER, repo3); + Task r3t2 = comTask("r3t2", TaskPriority.LISTENER, repo3); + Task r3t3 = comTask("r3t3", TaskPriority.LISTENER, repo3); + + List tasks = List.of(r1t3, r3t2, r2t1, r1t1, r1t2, r3t3, r3t1, r1t4); + List expected = List.of(r1t4, r2t1, r3t3, r1t3, r3t2, r1t2, r3t1, r1t1); + + assertThat(useStepAll(tasks, repo1)).isEqualTo(expected); + assertThat(simulQueue(tasks, repo1)).isEqualTo(expected); + } + + @Test + void manualBeforeTarBeforeListener() { + Task m1 = comTask("m1", TaskPriority.MANUAL, repo1); + Task m2 = tarTask("m2", TaskPriority.MANUAL, null); + + Task t1 = tarTask("t1", TaskPriority.USER_CREATED, repo2); + Task t2 = tarTask("t2", TaskPriority.USER_CREATED, null); + + Task l1 = comTask("l1", TaskPriority.LISTENER, repo1); + Task l2 = comTask("l2", TaskPriority.LISTENER, repo2); + Task l3 = comTask("l3", TaskPriority.LISTENER, repo2); + + List tasks = List.of(l2, t1, t2, m2, l3, m1, l1); + List expected = List.of(m2, m1, t1, t2, l1, l3, l2); + + assertThat(useStepAll(tasks, repo1)).isEqualTo(expected); + assertThat(simulQueue(tasks, repo1)).isEqualTo(expected); + } + + @Test + void findCorrectNextRepo() { + Task r1 = comTask("r1", TaskPriority.LISTENER, repo1); + Task r2 = comTask("r2", TaskPriority.LISTENER, repo2); + Task r4 = comTask("r4", TaskPriority.LISTENER, repo4); + + List tasks = List.of(r4, r2, r1); + + List expected1 = List.of(r1, r2, r4); + assertThat(useStepAll(tasks, null)).isEqualTo(expected1); + assertThat(simulQueue(tasks, null)).isEqualTo(expected1); + + List expected2 = List.of(r2, r4, r1); + assertThat(useStepAll(tasks, repo2)).isEqualTo(expected2); + assertThat(simulQueue(tasks, repo2)).isEqualTo(expected2); + + List expected3 = List.of(r4, r1, r2); + assertThat(useStepAll(tasks, repo3)).isEqualTo(expected3); + assertThat(simulQueue(tasks, repo3)).isEqualTo(expected3); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractorTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractorTest.java index 69787de9d..1273fdda2 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractorTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/listener/github/GithubPrInteractorTest.java @@ -14,82 +14,77 @@ class GithubPrInteractorTest { - @Test - void buildSuccessfulPrReply() { - StringBuilder builder = new StringBuilder(); + @Test + void buildSuccessfulPrReply() { + StringBuilder builder = new StringBuilder(); - List differences = List.of( - new DimensionDifference( - new Dimension("bin/lean", "binary size"), - 53_422_616.0, 92_456.0, new RunId(), null - ), - new DimensionDifference( - new Dimension("binarytrees", "branch-misses"), - 86_454_404.5, 95_712_731.1, new RunId(), 500000.0 - ), - new DimensionDifference( - new Dimension("binarytrees", "instructions"), - 62_480_581_237.0, 64_322_330_171.1, new RunId(), 1000000.0 - ), - new DimensionDifference( - new Dimension("deriv", "maxrss"), - 0.0, 10.0, new RunId(), 1.0 - ), - new DimensionDifference( - new Dimension("const_fold", "branches"), - 100.0, 10.0, new RunId(), null - ) - ); - List failed = List.of( - new Dimension("const_fold", "instructions") - ); - Map infos = Map.of( - new Dimension("bin/lean", "binary size"), - new DimensionInfo( - new Dimension("bin/lean", "binary size"), - new Unit("binary size"), - Interpretation.LESS_IS_BETTER - ), - new Dimension("binarytrees", "branch-misses"), - new DimensionInfo( - new Dimension("binarytrees", "branch-misses"), - new Unit("branch-misses"), - Interpretation.LESS_IS_BETTER - ), - new Dimension("binarytrees", "instructions"), - new DimensionInfo( - new Dimension("binarytrees", "instructions"), - new Unit("instructions"), - Interpretation.LESS_IS_BETTER - ), - new Dimension("deriv", "maxrss"), - new DimensionInfo( - new Dimension("deriv", "maxrss"), - new Unit("thingies"), - Interpretation.MORE_IS_BETTER - ), - new Dimension("const_fold", "branches"), - new DimensionInfo( - new Dimension("const_fold", "branches"), - new Unit("bits and bobs"), - Interpretation.NEUTRAL - ) - ); - GithubPrInteractor.buildSignificanceDiff(builder, differences, failed, infos); + List differences = + List.of( + new DimensionDifference( + new Dimension("bin/lean", "binary size"), + 53_422_616.0, + 92_456.0, + new RunId(), + null), + new DimensionDifference( + new Dimension("binarytrees", "branch-misses"), + 86_454_404.5, + 95_712_731.1, + new RunId(), + 500000.0), + new DimensionDifference( + new Dimension("binarytrees", "instructions"), + 62_480_581_237.0, + 64_322_330_171.1, + new RunId(), + 1000000.0), + new DimensionDifference(new Dimension("deriv", "maxrss"), 0.0, 10.0, new RunId(), 1.0), + new DimensionDifference( + new Dimension("const_fold", "branches"), 100.0, 10.0, new RunId(), null)); + List failed = List.of(new Dimension("const_fold", "instructions")); + Map infos = + Map.of( + new Dimension("bin/lean", "binary size"), + new DimensionInfo( + new Dimension("bin/lean", "binary size"), + new Unit("binary size"), + Interpretation.LESS_IS_BETTER), + new Dimension("binarytrees", "branch-misses"), + new DimensionInfo( + new Dimension("binarytrees", "branch-misses"), + new Unit("branch-misses"), + Interpretation.LESS_IS_BETTER), + new Dimension("binarytrees", "instructions"), + new DimensionInfo( + new Dimension("binarytrees", "instructions"), + new Unit("instructions"), + Interpretation.LESS_IS_BETTER), + new Dimension("deriv", "maxrss"), + new DimensionInfo( + new Dimension("deriv", "maxrss"), + new Unit("thingies"), + Interpretation.MORE_IS_BETTER), + new Dimension("const_fold", "branches"), + new DimensionInfo( + new Dimension("const_fold", "branches"), + new Unit("bits and bobs"), + Interpretation.NEUTRAL)); + GithubPrInteractor.buildSignificanceDiff(builder, differences, failed, infos); - String target = "" - + "\n```diff" - + "\n Benchmark Metric Change" - + "\n ===============================================" - + "\n+ bin/lean binary size -99.8%" - + "\n- binarytrees branch-misses 10.7% (18.5 σ)" - + "\n- binarytrees instructions 2.9% (1841.7 σ)" - + "\n const_fold branches -90.0%" - + "\n- const_fold instructions failed" - + "\n+ deriv maxrss - (10.0 σ)" - + "\n```"; + String target = + "" + + "\n```diff" + + "\n Benchmark Metric Change" + + "\n ===============================================" + + "\n+ bin/lean binary size -99.8%" + + "\n- binarytrees branch-misses 10.7% (18.5 σ)" + + "\n- binarytrees instructions 2.9% (1841.7 σ)" + + "\n const_fold branches -90.0%" + + "\n- const_fold instructions failed" + + "\n+ deriv maxrss - (10.0 σ)" + + "\n```"; - System.out.println(builder.toString()); - assertThat(builder.toString()).isEqualTo(target); - } + System.out.println(builder.toString()); + assertThat(builder.toString()).isEqualTo(target); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtilsTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtilsTest.java index 928dd2b7d..7ac4c2627 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtilsTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/endpoints/utils/EndpointUtilsTest.java @@ -9,21 +9,20 @@ class EndpointUtilsTest { - @Test - void parseValidColonSeparatedArgsCorrectly() { - String args = "hello:world:out:there::goodbye:and:farewell"; - List>> parsedArgs = EndpointUtils.parseColonSeparatedArgs(args); - assertThat(parsedArgs).isEqualTo( - List.of( - new Pair<>("hello", List.of("world", "out", "there")), - new Pair<>("goodbye", List.of("and", "farewell")) - ) - ); - } + @Test + void parseValidColonSeparatedArgsCorrectly() { + String args = "hello:world:out:there::goodbye:and:farewell"; + List>> parsedArgs = EndpointUtils.parseColonSeparatedArgs(args); + assertThat(parsedArgs) + .isEqualTo( + List.of( + new Pair<>("hello", List.of("world", "out", "there")), + new Pair<>("goodbye", List.of("and", "farewell")))); + } - @Test - void throwErrorOnInvalidColonSeparatedArgs() { - String args = "the:second:section:has:too:few:elements::see?::the:third:section:is:fine:though"; - assertThrows(ArgumentParseException.class, () -> EndpointUtils.parseColonSeparatedArgs(args)); - } + @Test + void throwErrorOnInvalidColonSeparatedArgs() { + String args = "the:second:section:has:too:few:elements::see?::the:third:section:is:fine:though"; + assertThrows(ArgumentParseException.class, () -> EndpointUtils.parseColonSeparatedArgs(args)); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescriptionTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescriptionTest.java index 1cb6217e6..288742260 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescriptionTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitDescriptionTest.java @@ -6,22 +6,23 @@ class JsonCommitDescriptionTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonCommitDescription( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - "e16272feb472dc4d357cc19dd97112c036a67990", - "authorName", - 1596881630, - "summaryText" - ); - String json = "{" - + "\"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + "\"hash\": \"e16272feb472dc4d357cc19dd97112c036a67990\"," - + "\"author\": \"authorName\"," - + "\"author_date\": 1596881630," - + "\"summary\": \"summaryText\"" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonCommitDescription( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + "e16272feb472dc4d357cc19dd97112c036a67990", + "authorName", + 1596881630, + "summaryText"); + String json = + "{" + + "\"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + "\"hash\": \"e16272feb472dc4d357cc19dd97112c036a67990\"," + + "\"author\": \"authorName\"," + + "\"author_date\": 1596881630," + + "\"summary\": \"summaryText\"" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitTest.java index 48473debd..7b2765549 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonCommitTest.java @@ -7,39 +7,40 @@ class JsonCommitTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonCommit( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - "e16272feb472dc4d357cc19dd97112c036a67990", - true, - List.of(), - List.of(), - List.of(), - List.of(), - "authorName", - 1596881630, - "committerName", - 1596881676, - "summaryText", - null, - List.of() - ); - String json = "{" - + "\"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + "\"hash\": \"e16272feb472dc4d357cc19dd97112c036a67990\"," - + "\"tracked\": true," - + "\"tracked_parents\": []," - + "\"untracked_parents\": []," - + "\"tracked_children\": []," - + "\"untracked_children\": []," - + "\"author\": \"authorName\"," - + "\"author_date\": 1596881630," - + "\"committer\": \"committerName\"," - + "\"committer_date\": 1596881676," - + "\"summary\": \"summaryText\"," - + "\"runs\": []" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonCommit( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + "e16272feb472dc4d357cc19dd97112c036a67990", + true, + List.of(), + List.of(), + List.of(), + List.of(), + "authorName", + 1596881630, + "committerName", + 1596881676, + "summaryText", + null, + List.of()); + String json = + "{" + + "\"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + "\"hash\": \"e16272feb472dc4d357cc19dd97112c036a67990\"," + + "\"tracked\": true," + + "\"tracked_parents\": []," + + "\"untracked_parents\": []," + + "\"tracked_children\": []," + + "\"untracked_children\": []," + + "\"author\": \"authorName\"," + + "\"author_date\": 1596881630," + + "\"committer\": \"committerName\"," + + "\"committer_date\": 1596881676," + + "\"summary\": \"summaryText\"," + + "\"runs\": []" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionInfoTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionInfoTest.java index dee86e7b6..5fa455b4b 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionInfoTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonDimensionInfoTest.java @@ -6,20 +6,17 @@ class JsonDimensionInfoTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonDimension( - "benchmarkName", - "metricName", - "someUnit", - Interpretation.LESS_IS_BETTER - ); - String json = "{" - + "\"benchmark\": \"benchmarkName\"," - + "\"metric\": \"metricName\"," - + "\"unit\": \"someUnit\"," - + "\"interpretation\": \"LESS_IS_BETTER\"" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonDimension("benchmarkName", "metricName", "someUnit", Interpretation.LESS_IS_BETTER); + String json = + "{" + + "\"benchmark\": \"benchmarkName\"," + + "\"metric\": \"metricName\"," + + "\"unit\": \"someUnit\"," + + "\"interpretation\": \"LESS_IS_BETTER\"" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurementTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurementTest.java index bed203365..b382d0dac 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurementTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonMeasurementTest.java @@ -7,67 +7,69 @@ class JsonMeasurementTest extends SerializingTest { - @Test - void serializeSuccessful() throws JsonProcessingException { - Object object = JsonMeasurement.successful( - new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), - 1.0, - List.of(2.0, 3.0), - 10.0, - 0.5 - ); - String json = "{" - + "\"dimension\": {" - + " \"benchmark\": \"b\"," - + " \"metric\": \"m\"," - + " \"unit\": \"u\"," - + " \"interpretation\": \"NEUTRAL\"" - + "}," - + "\"value\": 1.0," - + "\"values\": [2.0, 3.0]," - + "\"stddev\": 10.0," - + "\"stddev_percent\": 0.5" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeSuccessful() throws JsonProcessingException { + Object object = + JsonMeasurement.successful( + new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), + 1.0, + List.of(2.0, 3.0), + 10.0, + 0.5); + String json = + "{" + + "\"dimension\": {" + + " \"benchmark\": \"b\"," + + " \"metric\": \"m\"," + + " \"unit\": \"u\"," + + " \"interpretation\": \"NEUTRAL\"" + + "}," + + "\"value\": 1.0," + + "\"values\": [2.0, 3.0]," + + "\"stddev\": 10.0," + + "\"stddev_percent\": 0.5" + + "}"; + serializedEquals(object, json); + } - @Test - void serializeSuccessfulWithoutStddev() throws JsonProcessingException { - Object object = JsonMeasurement.successful( - new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), - 1.0, - List.of(2.0, 3.0), - null, - null - ); - String json = "{" - + "\"dimension\": {" - + " \"benchmark\": \"b\"," - + " \"metric\": \"m\"," - + " \"unit\": \"u\"," - + " \"interpretation\": \"NEUTRAL\"" - + "}," - + "\"value\": 1.0," - + "\"values\": [2.0, 3.0]" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeSuccessfulWithoutStddev() throws JsonProcessingException { + Object object = + JsonMeasurement.successful( + new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), + 1.0, + List.of(2.0, 3.0), + null, + null); + String json = + "{" + + "\"dimension\": {" + + " \"benchmark\": \"b\"," + + " \"metric\": \"m\"," + + " \"unit\": \"u\"," + + " \"interpretation\": \"NEUTRAL\"" + + "}," + + "\"value\": 1.0," + + "\"values\": [2.0, 3.0]" + + "}"; + serializedEquals(object, json); + } - @Test - void serializeFailed() throws JsonProcessingException { - Object object = JsonMeasurement.failed( - new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), - "errorText" - ); - String json = "{" - + "\"dimension\": {" - + " \"benchmark\": \"b\"," - + " \"metric\": \"m\"," - + " \"unit\": \"u\"," - + " \"interpretation\": \"NEUTRAL\"" - + "}," - + "\"error\": \"errorText\"" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeFailed() throws JsonProcessingException { + Object object = + JsonMeasurement.failed( + new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), "errorText"); + String json = + "{" + + "\"dimension\": {" + + " \"benchmark\": \"b\"," + + " \"metric\": \"m\"," + + " \"unit\": \"u\"," + + " \"interpretation\": \"NEUTRAL\"" + + "}," + + "\"error\": \"errorText\"" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepoTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepoTest.java index ceab5119a..f4da515a8 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepoTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRepoTest.java @@ -8,37 +8,37 @@ class JsonRepoTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonRepo( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - "velcom", - "https://vel.com/", - List.of( - new JsonBranch("main", true, "foo"), - new JsonBranch("untracked", false, "bar"), - new JsonBranch("branches", false, "baz") - ), - List.of(new JsonDimension("b", "m", "u", Interpretation.NEUTRAL)), - 1596881630L - ); - String json = "{" - + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + "\"name\": \"velcom\"," - + "\"remote_url\": \"https://vel.com/\"," - + "\"branches\": [" - + " {\"name\": \"main\", \"tracked\": true, \"latest_commit\": \"foo\"}," - + " {\"name\": \"untracked\", \"tracked\": false, \"latest_commit\": \"bar\"}," - + " {\"name\": \"branches\", \"tracked\": false, \"latest_commit\": \"baz\"}" - + "]," - + "\"dimensions\": [{" - + " \"benchmark\": \"b\"," - + " \"metric\": \"m\"," - + " \"unit\": \"u\"," - + " \"interpretation\": \"NEUTRAL\"" - + "}]," - + "\"last_github_update\": 1596881630" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonRepo( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + "velcom", + "https://vel.com/", + List.of( + new JsonBranch("main", true, "foo"), + new JsonBranch("untracked", false, "bar"), + new JsonBranch("branches", false, "baz")), + List.of(new JsonDimension("b", "m", "u", Interpretation.NEUTRAL)), + 1596881630L); + String json = + "{" + + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + "\"name\": \"velcom\"," + + "\"remote_url\": \"https://vel.com/\"," + + "\"branches\": [" + + " {\"name\": \"main\", \"tracked\": true, \"latest_commit\": \"foo\"}," + + " {\"name\": \"untracked\", \"tracked\": false, \"latest_commit\": \"bar\"}," + + " {\"name\": \"branches\", \"tracked\": false, \"latest_commit\": \"baz\"}" + + "]," + + "\"dimensions\": [{" + + " \"benchmark\": \"b\"," + + " \"metric\": \"m\"," + + " \"unit\": \"u\"," + + " \"interpretation\": \"NEUTRAL\"" + + "}]," + + "\"last_github_update\": 1596881630" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResultTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResultTest.java index 39ca70885..48cb36a9c 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResultTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonResultTest.java @@ -7,43 +7,39 @@ class JsonResultTest extends SerializingTest { - @Test - void serializeSuccessful() throws JsonProcessingException { - Object object = JsonResult.successful( - List.of(JsonMeasurement.failed( - new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), - "errorText" - )) - ); - String json = "{" - + "\"measurements\": [{" - + " \"dimension\": {" - + " \"benchmark\": \"b\"," - + " \"metric\": \"m\"," - + " \"unit\": \"u\"," - + " \"interpretation\": \"NEUTRAL\"" - + " }," - + " \"error\": \"errorText\"" - + "}]" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeSuccessful() throws JsonProcessingException { + Object object = + JsonResult.successful( + List.of( + JsonMeasurement.failed( + new JsonDimension("b", "m", "u", Interpretation.NEUTRAL), "errorText"))); + String json = + "{" + + "\"measurements\": [{" + + " \"dimension\": {" + + " \"benchmark\": \"b\"," + + " \"metric\": \"m\"," + + " \"unit\": \"u\"," + + " \"interpretation\": \"NEUTRAL\"" + + " }," + + " \"error\": \"errorText\"" + + "}]" + + "}"; + serializedEquals(object, json); + } - @Test - void serializeBenchError() throws JsonProcessingException { - Object object = JsonResult.benchError("benchErrorText"); - String json = "{" - + "\"bench_error\": \"benchErrorText\"" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeBenchError() throws JsonProcessingException { + Object object = JsonResult.benchError("benchErrorText"); + String json = "{" + "\"bench_error\": \"benchErrorText\"" + "}"; + serializedEquals(object, json); + } - @Test - void serializeVelcomError() throws JsonProcessingException { - Object object = JsonResult.velcomError("velcomErrorText"); - String json = "{" - + "\"velcom_error\": \"velcomErrorText\"" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeVelcomError() throws JsonProcessingException { + Object object = JsonResult.velcomError("velcomErrorText"); + String json = "{" + "\"velcom_error\": \"velcomErrorText\"" + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescriptionTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescriptionTest.java index fc9ee485a..f35df5ea2 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescriptionTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunDescriptionTest.java @@ -7,26 +7,24 @@ class JsonRunDescriptionTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonRunDescription( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - 1596881630, - JsonSuccess.SUCCESS, - JsonSource.tarSource( - "descriptionText", - null - ) - ); - String json = "{" - + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + "\"start_time\": 1596881630," - + "\"success\": \"SUCCESS\"," - + "\"source\": {" - + " \"type\": \"UPLOADED_TAR\"," - + " \"source\": {\"description\": \"descriptionText\"}" - + "}" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonRunDescription( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + 1596881630, + JsonSuccess.SUCCESS, + JsonSource.tarSource("descriptionText", null)); + String json = + "{" + + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + "\"start_time\": 1596881630," + + "\"success\": \"SUCCESS\"," + + "\"source\": {" + + " \"type\": \"UPLOADED_TAR\"," + + " \"source\": {\"description\": \"descriptionText\"}" + + "}" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunTest.java index 43f8fd90f..c6be677b9 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonRunTest.java @@ -6,31 +6,32 @@ class JsonRunTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonRun( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - "authorName", - "runnerName", - "runnerInfoText", - 1596881630, - 1596881676, - JsonSource.tarSource("descriptionText", null), - JsonResult.velcomError("velcomErrorText") - ); - String json = "{" - + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + "\"author\": \"authorName\"," - + "\"runner_name\": \"runnerName\"," - + "\"runner_info\": \"runnerInfoText\"," - + "\"start_time\": 1596881630," - + "\"stop_time\": 1596881676," - + "\"source\": {" - + " \"type\": \"UPLOADED_TAR\"," - + " \"source\": {\"description\": \"descriptionText\"}" - + "}," - + "\"result\": {\"velcom_error\": \"velcomErrorText\"}" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonRun( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + "authorName", + "runnerName", + "runnerInfoText", + 1596881630, + 1596881676, + JsonSource.tarSource("descriptionText", null), + JsonResult.velcomError("velcomErrorText")); + String json = + "{" + + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + "\"author\": \"authorName\"," + + "\"runner_name\": \"runnerName\"," + + "\"runner_info\": \"runnerInfoText\"," + + "\"start_time\": 1596881630," + + "\"stop_time\": 1596881676," + + "\"source\": {" + + " \"type\": \"UPLOADED_TAR\"," + + " \"source\": {\"description\": \"descriptionText\"}" + + "}," + + "\"result\": {\"velcom_error\": \"velcomErrorText\"}" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSourceTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSourceTest.java index e2b733c7a..fdba65f80 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSourceTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonSourceTest.java @@ -6,53 +6,53 @@ class JsonSourceTest extends SerializingTest { - @Test - void serializeFromCommit() throws JsonProcessingException { - Object object = JsonSource.commitSource(new JsonCommitDescription( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - "e16272feb472dc4d357cc19dd97112c036a67990", - "authorName", - 1596881630, - "summaryText" - )); - String json = "{" - + "\"type\": \"COMMIT\"," - + "\"source\": {" - + " \"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + " \"hash\": \"e16272feb472dc4d357cc19dd97112c036a67990\"," - + " \"author\": \"authorName\"," - + " \"author_date\": 1596881630," - + " \"summary\": \"summaryText\"" - + "}" - + "}"; - serializedEquals(object, json); - } + @Test + void serializeFromCommit() throws JsonProcessingException { + Object object = + JsonSource.commitSource( + new JsonCommitDescription( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + "e16272feb472dc4d357cc19dd97112c036a67990", + "authorName", + 1596881630, + "summaryText")); + String json = + "{" + + "\"type\": \"COMMIT\"," + + "\"source\": {" + + " \"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + " \"hash\": \"e16272feb472dc4d357cc19dd97112c036a67990\"," + + " \"author\": \"authorName\"," + + " \"author_date\": 1596881630," + + " \"summary\": \"summaryText\"" + + "}" + + "}"; + serializedEquals(object, json); + } - @Test - void serializeFromUploadedTar() throws JsonProcessingException { - Object object = JsonSource.tarSource( - "descriptionText", - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6") - ); - String json = "{" - + "\"type\": \"UPLOADED_TAR\"," - + "\"source\": {" - + " \"description\": \"descriptionText\"," - + " \"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"" - + "}" - + "}"; - serializedEquals(object, json); + @Test + void serializeFromUploadedTar() throws JsonProcessingException { + Object object = + JsonSource.tarSource( + "descriptionText", UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6")); + String json = + "{" + + "\"type\": \"UPLOADED_TAR\"," + + "\"source\": {" + + " \"description\": \"descriptionText\"," + + " \"repo_id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"" + + "}" + + "}"; + serializedEquals(object, json); - object = JsonSource.tarSource( - "descriptionText", - null - ); - json = "{" - + "\"type\": \"UPLOADED_TAR\"," - + "\"source\": {" - + " \"description\": \"descriptionText\"" - + "}" - + "}"; - serializedEquals(object, json); - } + object = JsonSource.tarSource("descriptionText", null); + json = + "{" + + "\"type\": \"UPLOADED_TAR\"," + + "\"source\": {" + + " \"description\": \"descriptionText\"" + + "}" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTaskTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTaskTest.java index ac3ab7fed..539cb9204 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTaskTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/JsonTaskTest.java @@ -6,23 +6,24 @@ class JsonTaskTest extends SerializingTest { - @Test - void serialize() throws JsonProcessingException { - Object object = new JsonTask( - UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), - "authorName", - 1596881630, - JsonSource.tarSource("descriptionText", null) - ); - String json = "{" - + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," - + "\"author\": \"authorName\"," - + "\"since\": 1596881630," - + "\"source\": {" - + " \"type\": \"UPLOADED_TAR\"," - + " \"source\": {\"description\": \"descriptionText\"}" - + "}" - + "}"; - serializedEquals(object, json); - } + @Test + void serialize() throws JsonProcessingException { + Object object = + new JsonTask( + UUID.fromString("24dd4fd3-5c6d-4542-a7a4-b181f37295a6"), + "authorName", + 1596881630, + JsonSource.tarSource("descriptionText", null)); + String json = + "{" + + "\"id\": \"24dd4fd3-5c6d-4542-a7a4-b181f37295a6\"," + + "\"author\": \"authorName\"," + + "\"since\": 1596881630," + + "\"source\": {" + + " \"type\": \"UPLOADED_TAR\"," + + " \"source\": {\"description\": \"descriptionText\"}" + + "}" + + "}"; + serializedEquals(object, json); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/SerializingTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/SerializingTest.java index 681d64525..86c9a2039 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/SerializingTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/restapi/jsonobjects/SerializingTest.java @@ -10,19 +10,20 @@ public abstract class SerializingTest { - protected ObjectMapper objectMapper; + protected ObjectMapper objectMapper; - @BeforeEach - void setUp() { - // This mapper should be configured the same as the one in ServerMain.java - objectMapper = new ObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) - .setSerializationInclusion(Include.NON_NULL); - } + @BeforeEach + void setUp() { + // This mapper should be configured the same as the one in ServerMain.java + objectMapper = + new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .setSerializationInclusion(Include.NON_NULL); + } - protected void serializedEquals(Object object, String json) throws JsonProcessingException { - JsonNode objectTree = objectMapper.readTree(objectMapper.writeValueAsString(object)); - JsonNode jsonTree = objectMapper.readTree(json); - Assertions.assertThat(objectTree).isEqualTo(jsonTree); - } + protected void serializedEquals(Object object, String json) throws JsonProcessingException { + JsonNode objectTree = objectMapper.readTree(objectMapper.writeValueAsString(object)); + JsonNode jsonTree = objectMapper.readTree(json); + Assertions.assertThat(objectTree).isEqualTo(jsonTree); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/DispatcherTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/DispatcherTest.java index 9740cc759..4e4a7e44f 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/DispatcherTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/DispatcherTest.java @@ -21,130 +21,122 @@ class DispatcherTest { - private Dispatcher dispatcher; - - private Queue queue; - private Duration runnerGracePeriod; - private KnownRunner knownRunner; - - @BeforeEach - void setUp() { - queue = mock(Queue.class); - when(queue.startNextTask()).thenReturn(Optional.of(mock(Task.class))); - - runnerGracePeriod = Duration.ofSeconds(1); - dispatcher = new Dispatcher(queue, runnerGracePeriod); - knownRunner = new KnownRunner( - "runner", - "info", - "hash", - Status.IDLE, - null, - false, - null, - null, - List.of() - ); - } - - @Test - void addedRunnerVisibleInAllRunners() { - TeleRunner runner = getRunner(); - dispatcher.addRunner(runner); - - assertThat(dispatcher.getKnownRunners()).containsExactly(knownRunner); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); - } - - @Test - void cantAddRunnerWithSameName() { - dispatcher.addRunner(getRunner()); - assertThatThrownBy(() -> dispatcher.addRunner(getRunner())) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("name"); - } - - @Test - void doesNotReturnWorkForUnregisteredRunner() { - assertThat(queue.startNextTask()).isPresent(); - assertThat(dispatcher.getWork(getRunner())).isEmpty(); - } - - @Test - void returnsWorkForRegisteredRunner() { - TeleRunner runner = getRunner(); - dispatcher.addRunner(runner); - - assertThat(queue.startNextTask()).isPresent(); - assertThat(dispatcher.getWork(runner)).isPresent(); - } - - @Test - void cleansUpDisconnectedRunner() throws InterruptedException { - AtomicBoolean returnInvalidPingTime = new AtomicBoolean(false); - TeleRunner runner = getRunner(); - when(runner.hasConnection()).thenReturn(false); - when(runner.getLastPing()).then(invocation -> returnInvalidPingTime.get() - ? Instant.now().minus(runnerGracePeriod).minusMillis(1) - : Instant.now() - ); - - dispatcher.addRunner(runner); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); - - returnInvalidPingTime.set(true); - - Thread.sleep(2000); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).isEmpty(); - } - - @Test - void doesNotCleanUpUnresponsiveConnectedRunner() throws InterruptedException { - TeleRunner runner = getRunner(); - when(runner.hasConnection()).thenReturn(true); - // Out of the grace period but it still has a connection. We keep it, as the websocket listener - // probably just reset the connection. We have a dedicated ping timeout. - when(runner.getLastPing()).then( - invocation -> Instant.now().minus(runnerGracePeriod).minusMillis(1) - ); - - dispatcher.addRunner(runner); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); - - Thread.sleep(2000); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); - } - - @Test - void doesNotCleanUpConnectedRunner() throws InterruptedException { - TeleRunner runner = getRunner(); - when(runner.hasConnection()).thenReturn(true); - when(runner.getLastPing()).then( - // Juuust inside - invocation -> Instant.now().minus(runnerGracePeriod) - ); - - dispatcher.addRunner(runner); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); - - Thread.sleep(2000); - assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); - } - - @Test - void completeWorkWritesThroughToQueue() { - NewRun newRun = mock(NewRun.class); - dispatcher.completeTask(newRun); - - verify(queue).completeTask(newRun); - } - - private TeleRunner getRunner() { - TeleRunner runner = mock(TeleRunner.class); - when(runner.getRunnerInformation()).thenReturn(knownRunner); - when(runner.hasConnection()).thenReturn(true); - when(runner.getRunnerName()).thenReturn(knownRunner.getName()); - when(runner.isDisposed()).thenReturn(false); - return runner; - } + private Dispatcher dispatcher; + + private Queue queue; + private Duration runnerGracePeriod; + private KnownRunner knownRunner; + + @BeforeEach + void setUp() { + queue = mock(Queue.class); + when(queue.startNextTask()).thenReturn(Optional.of(mock(Task.class))); + + runnerGracePeriod = Duration.ofSeconds(1); + dispatcher = new Dispatcher(queue, runnerGracePeriod); + knownRunner = + new KnownRunner("runner", "info", "hash", Status.IDLE, null, false, null, null, List.of()); + } + + @Test + void addedRunnerVisibleInAllRunners() { + TeleRunner runner = getRunner(); + dispatcher.addRunner(runner); + + assertThat(dispatcher.getKnownRunners()).containsExactly(knownRunner); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); + } + + @Test + void cantAddRunnerWithSameName() { + dispatcher.addRunner(getRunner()); + assertThatThrownBy(() -> dispatcher.addRunner(getRunner())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("name"); + } + + @Test + void doesNotReturnWorkForUnregisteredRunner() { + assertThat(queue.startNextTask()).isPresent(); + assertThat(dispatcher.getWork(getRunner())).isEmpty(); + } + + @Test + void returnsWorkForRegisteredRunner() { + TeleRunner runner = getRunner(); + dispatcher.addRunner(runner); + + assertThat(queue.startNextTask()).isPresent(); + assertThat(dispatcher.getWork(runner)).isPresent(); + } + + @Test + void cleansUpDisconnectedRunner() throws InterruptedException { + AtomicBoolean returnInvalidPingTime = new AtomicBoolean(false); + TeleRunner runner = getRunner(); + when(runner.hasConnection()).thenReturn(false); + when(runner.getLastPing()) + .then( + invocation -> + returnInvalidPingTime.get() + ? Instant.now().minus(runnerGracePeriod).minusMillis(1) + : Instant.now()); + + dispatcher.addRunner(runner); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); + + returnInvalidPingTime.set(true); + + Thread.sleep(2000); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).isEmpty(); + } + + @Test + void doesNotCleanUpUnresponsiveConnectedRunner() throws InterruptedException { + TeleRunner runner = getRunner(); + when(runner.hasConnection()).thenReturn(true); + // Out of the grace period but it still has a connection. We keep it, as the websocket listener + // probably just reset the connection. We have a dedicated ping timeout. + when(runner.getLastPing()) + .then(invocation -> Instant.now().minus(runnerGracePeriod).minusMillis(1)); + + dispatcher.addRunner(runner); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); + + Thread.sleep(2000); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); + } + + @Test + void doesNotCleanUpConnectedRunner() throws InterruptedException { + TeleRunner runner = getRunner(); + when(runner.hasConnection()).thenReturn(true); + when(runner.getLastPing()) + .then( + // Juuust inside + invocation -> Instant.now().minus(runnerGracePeriod)); + + dispatcher.addRunner(runner); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); + + Thread.sleep(2000); + assertThat(dispatcher.getTeleRunner(runner.getRunnerName())).get().isSameAs(runner); + } + + @Test + void completeWorkWritesThroughToQueue() { + NewRun newRun = mock(NewRun.class); + dispatcher.completeTask(newRun); + + verify(queue).completeTask(newRun); + } + + private TeleRunner getRunner() { + TeleRunner runner = mock(TeleRunner.class); + when(runner.getRunnerInformation()).thenReturn(knownRunner); + when(runner.hasConnection()).thenReturn(true); + when(runner.getRunnerName()).thenReturn(knownRunner.getName()); + when(runner.isDisposed()).thenReturn(false); + return runner; + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServletTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServletTest.java index 4d3a24376..d0ce8ecbe 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServletTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/ServerMasterWebsocketServletTest.java @@ -24,135 +24,130 @@ class ServerMasterWebsocketServletTest { - private ServerMasterWebsocketServlet servlet; - private String runnerName; - - private WebSocketCreator creator; - private String runnerToken; - private ServletUpgradeRequest upgradeRequest; - private ServletUpgradeResponse upgradeResponse; - private Dispatcher dispatcher; - - @BeforeEach - void setUp() { - runnerToken = "runner token"; - runnerName = "name"; - - this.upgradeRequest = mock(ServletUpgradeRequest.class); - when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName())) - .thenReturn(runnerName); - when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName())) - .thenReturn(runnerToken); - - upgradeResponse = mock(ServletUpgradeResponse.class); - dispatcher = mock(Dispatcher.class); - - servlet = new ServerMasterWebsocketServlet( - dispatcher, - mock(Serializer.class), - runnerToken, - mock(BenchRepo.class) - ); - WebSocketServerFactory factory = new WebSocketServerFactory(); - servlet.configure(factory); - this.creator = factory.getCreator(); - } - - @Test - void rejectsConnectionWithoutAuthHeader() throws Exception { - when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName())) - .thenReturn(null); - - assertRejectedInvalidToken(); - } - - @Test - void rejectsConnectionWithoutName() throws Exception { - when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName())) - .thenReturn(null); - - assertRejectedInvalidToken(); - } - - @Test - void rejectsConnectionWithInvalidToken() throws Exception { - when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName())) - .thenReturn("hello"); - - assertRejectedInvalidToken(); - } - - @Test - void rejectsNameAlreadyTaken() throws Exception { - TeleRunner runner = mock(TeleRunner.class); - when(runner.hasConnection()).thenReturn(true); - when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.of(runner)); - - assertThat(requestConnection()).isNull(); - verify(upgradeResponse).sendError( - RunnerDenyReason.NAME_ALREADY_USED.getCode(), - RunnerDenyReason.NAME_ALREADY_USED.getMessage() - ); - verify(upgradeResponse).addHeader( - RunnerConnectionHeader.DISCONNECT_DENY_REASON.getName(), - RunnerDenyReason.NAME_ALREADY_USED.getHeaderValue() - ); - verify(dispatcher, never()).addRunner(any()); - } - - @Test - void allowsSameNameWhenDisconnected() { - TeleRunner runner = mock(TeleRunner.class); - RunnerConnection connection = mock(RunnerConnection.class); - - when(runner.hasConnection()).thenReturn(false); - when(runner.createConnection()).thenReturn(connection); - when(runner.isDisposed()).thenReturn(false); - when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.of(runner)); - - assertThat(requestConnection()).isSameAs(connection); - // Does not yet add it, as it is a new runner and needs to be ready first - verify(dispatcher, never()).addRunner(runner); - } - - @Test - void allowsSameNameWhenDisconnectedAndDisposed() { - TeleRunner runner = mock(TeleRunner.class); - RunnerConnection connection = mock(RunnerConnection.class); - when(runner.hasConnection()).thenReturn(false); - when(runner.createConnection()).thenReturn(connection); - when(runner.isDisposed()).thenReturn(true); - when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.of(runner)); - - assertThat(requestConnection()).isNotNull().isNotSameAs(connection); - // Does not yet add it, as it is a new runner and needs to be ready first - verify(dispatcher, never()).addRunner(runner); - } - - @Test - void acceptsNewRunner() { - when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.empty()); - - assertThat(requestConnection()).isNotNull(); - // Does not yet add it, as it is a new runner and needs to be ready first - verify(dispatcher, never()).addRunner(any()); - } - - private void assertRejectedInvalidToken() throws IOException { - assertThat(requestConnection()).isNull(); - verify(upgradeResponse).sendError( - RunnerDenyReason.TOKEN_INVALID.getCode(), - RunnerDenyReason.TOKEN_INVALID.getMessage() - ); - verify(upgradeResponse).addHeader( - RunnerConnectionHeader.DISCONNECT_DENY_REASON.getName(), - RunnerDenyReason.TOKEN_INVALID.getHeaderValue() - ); - verify(dispatcher, never()).addRunner(any()); - } - - private Object requestConnection() { - return creator.createWebSocket(upgradeRequest, upgradeResponse); - } - + private ServerMasterWebsocketServlet servlet; + private String runnerName; + + private WebSocketCreator creator; + private String runnerToken; + private ServletUpgradeRequest upgradeRequest; + private ServletUpgradeResponse upgradeResponse; + private Dispatcher dispatcher; + + @BeforeEach + void setUp() { + runnerToken = "runner token"; + runnerName = "name"; + + this.upgradeRequest = mock(ServletUpgradeRequest.class); + when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName())) + .thenReturn(runnerName); + when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName())) + .thenReturn(runnerToken); + + upgradeResponse = mock(ServletUpgradeResponse.class); + dispatcher = mock(Dispatcher.class); + + servlet = + new ServerMasterWebsocketServlet( + dispatcher, mock(Serializer.class), runnerToken, mock(BenchRepo.class)); + WebSocketServerFactory factory = new WebSocketServerFactory(); + servlet.configure(factory); + this.creator = factory.getCreator(); + } + + @Test + void rejectsConnectionWithoutAuthHeader() throws Exception { + when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName())) + .thenReturn(null); + + assertRejectedInvalidToken(); + } + + @Test + void rejectsConnectionWithoutName() throws Exception { + when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName())) + .thenReturn(null); + + assertRejectedInvalidToken(); + } + + @Test + void rejectsConnectionWithInvalidToken() throws Exception { + when(upgradeRequest.getHeader(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName())) + .thenReturn("hello"); + + assertRejectedInvalidToken(); + } + + @Test + void rejectsNameAlreadyTaken() throws Exception { + TeleRunner runner = mock(TeleRunner.class); + when(runner.hasConnection()).thenReturn(true); + when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.of(runner)); + + assertThat(requestConnection()).isNull(); + verify(upgradeResponse) + .sendError( + RunnerDenyReason.NAME_ALREADY_USED.getCode(), + RunnerDenyReason.NAME_ALREADY_USED.getMessage()); + verify(upgradeResponse) + .addHeader( + RunnerConnectionHeader.DISCONNECT_DENY_REASON.getName(), + RunnerDenyReason.NAME_ALREADY_USED.getHeaderValue()); + verify(dispatcher, never()).addRunner(any()); + } + + @Test + void allowsSameNameWhenDisconnected() { + TeleRunner runner = mock(TeleRunner.class); + RunnerConnection connection = mock(RunnerConnection.class); + + when(runner.hasConnection()).thenReturn(false); + when(runner.createConnection()).thenReturn(connection); + when(runner.isDisposed()).thenReturn(false); + when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.of(runner)); + + assertThat(requestConnection()).isSameAs(connection); + // Does not yet add it, as it is a new runner and needs to be ready first + verify(dispatcher, never()).addRunner(runner); + } + + @Test + void allowsSameNameWhenDisconnectedAndDisposed() { + TeleRunner runner = mock(TeleRunner.class); + RunnerConnection connection = mock(RunnerConnection.class); + when(runner.hasConnection()).thenReturn(false); + when(runner.createConnection()).thenReturn(connection); + when(runner.isDisposed()).thenReturn(true); + when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.of(runner)); + + assertThat(requestConnection()).isNotNull().isNotSameAs(connection); + // Does not yet add it, as it is a new runner and needs to be ready first + verify(dispatcher, never()).addRunner(runner); + } + + @Test + void acceptsNewRunner() { + when(dispatcher.getTeleRunner(runnerName)).thenReturn(Optional.empty()); + + assertThat(requestConnection()).isNotNull(); + // Does not yet add it, as it is a new runner and needs to be ready first + verify(dispatcher, never()).addRunner(any()); + } + + private void assertRejectedInvalidToken() throws IOException { + assertThat(requestConnection()).isNull(); + verify(upgradeResponse) + .sendError( + RunnerDenyReason.TOKEN_INVALID.getCode(), RunnerDenyReason.TOKEN_INVALID.getMessage()); + verify(upgradeResponse) + .addHeader( + RunnerConnectionHeader.DISCONNECT_DENY_REASON.getName(), + RunnerDenyReason.TOKEN_INVALID.getHeaderValue()); + verify(dispatcher, never()).addRunner(any()); + } + + private Object requestConnection() { + return creator.createWebSocket(upgradeRequest, upgradeResponse); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequesterTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequesterTest.java index 059c2f52c..c1abc1dd6 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequesterTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/PeriodicStatusRequesterTest.java @@ -34,196 +34,197 @@ class PeriodicStatusRequesterTest { - private static final UUID RUN_ID = UUID.randomUUID(); - - private PeriodicStatusRequester statusRequester; - private StateMachine stateMachine; - private TeleRunner teleRunner; - private RunnerConnection runnerConnection; - - @BeforeEach - void setUp() { - //noinspection unchecked - stateMachine = mock(StateMachine.class); - teleRunner = mock(TeleRunner.class); - runnerConnection = mock(RunnerConnection.class); - when(runnerConnection.getSerializer()).thenReturn(new Serializer()); - - this.statusRequester = new PeriodicStatusRequester( - teleRunner, - runnerConnection, - stateMachine - ); - } - - @AfterEach - void tearDown() { - this.statusRequester.cancel(); - } - - @Test - void requestsStatus() throws InterruptedException { - statusRequester.start(); - - verify(stateMachine, timeout(1000)).switchFromRestingState(any(AwaitGetStatusReply.class)); - verify(runnerConnection, timeout(1000)) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_STATUS)); - } - - @Test - void doesNotRequestStatusTooOften() throws InterruptedException { - statusRequester.start(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(AwaitGetStatusReply.class); - verify(stateMachine, timeout(1000)).switchFromRestingState(captor.capture()); - verify(runnerConnection, timeout(1000)) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_STATUS)); - - captor.getValue().getReplyFuture().complete(new GetStatusReply( - "info", "version", "version", false, Status.IDLE, null, null - )); - - verify(runnerConnection, after(2000).atMostOnce()) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_STATUS)); - } - - @Test - void doesNotRequestResultsIfNone() { - statusRequester.start(); - - verify(runnerConnection, after(1000).never()) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); - } - - @Test - void requestResultsIfPresent() throws InterruptedException { - statusRequester.start(); - - var captor = ArgumentCaptor.forClass(AwaitGetStatusReply.class); - verify(stateMachine, timeout(1000)).switchFromRestingState(captor.capture()); - - captor.getValue().getReplyFuture().complete(new GetStatusReply( - "info", "version", "version", true, Status.IDLE, null, null - )); - - verify(runnerConnection, timeout(5000)) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); - } - - @Test - void clearResultsIfRunnerHasNoTask() throws InterruptedException { - when(teleRunner.getCurrentTask()).thenReturn(Optional.empty()); - getResult(); - - expectClearResultsAndConsumeIt(); - } - - @Test - void clearResultsIfRunnerHasDifferentTask() throws InterruptedException { - Task task = mock(Task.class); - when(task.getId()).thenReturn(new TaskId(UUID.randomUUID())); - when(teleRunner.getCurrentTask()).thenReturn(Optional.of(task)); - getResult(); - - expectClearResultsAndConsumeIt(); - verify(runnerConnection, timeout(5000)).close(StatusCode.ILLEGAL_BEHAVIOUR); - } - - @Test - void savesResultIfRunnerHasSameTask() throws InterruptedException { - Task task = mock(Task.class); - when(task.getId()).thenReturn(new TaskId(RUN_ID)); - when(teleRunner.getCurrentTask()).thenReturn(Optional.of(task)); - GetResultReply result = getResult(); - - expectClearResultsAndConsumeIt(); - - verify(teleRunner, timeout(1000)).handleResults(result); - } - - @Test - void clearResultIfConnectionHasError() throws InterruptedException { - getStatusReplyFuture().completeExceptionally(new RuntimeException("A!")); - - expectClearResultsAndConsumeIt(); - } + private static final UUID RUN_ID = UUID.randomUUID(); + + private PeriodicStatusRequester statusRequester; + private StateMachine stateMachine; + private TeleRunner teleRunner; + private RunnerConnection runnerConnection; + + @BeforeEach + void setUp() { + //noinspection unchecked + stateMachine = mock(StateMachine.class); + teleRunner = mock(TeleRunner.class); + runnerConnection = mock(RunnerConnection.class); + when(runnerConnection.getSerializer()).thenReturn(new Serializer()); + + this.statusRequester = new PeriodicStatusRequester(teleRunner, runnerConnection, stateMachine); + } + + @AfterEach + void tearDown() { + this.statusRequester.cancel(); + } + + @Test + void requestsStatus() throws InterruptedException { + statusRequester.start(); + + verify(stateMachine, timeout(1000)).switchFromRestingState(any(AwaitGetStatusReply.class)); + verify(runnerConnection, timeout(1000)) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_STATUS)); + } + + @Test + void doesNotRequestStatusTooOften() throws InterruptedException { + statusRequester.start(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AwaitGetStatusReply.class); + verify(stateMachine, timeout(1000)).switchFromRestingState(captor.capture()); + verify(runnerConnection, timeout(1000)) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_STATUS)); + + captor + .getValue() + .getReplyFuture() + .complete(new GetStatusReply("info", "version", "version", false, Status.IDLE, null, null)); + + verify(runnerConnection, after(2000).atMostOnce()) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_STATUS)); + } + + @Test + void doesNotRequestResultsIfNone() { + statusRequester.start(); + + verify(runnerConnection, after(1000).never()) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); + } + + @Test + void requestResultsIfPresent() throws InterruptedException { + statusRequester.start(); + + var captor = ArgumentCaptor.forClass(AwaitGetStatusReply.class); + verify(stateMachine, timeout(1000)).switchFromRestingState(captor.capture()); + + captor + .getValue() + .getReplyFuture() + .complete(new GetStatusReply("info", "version", "version", true, Status.IDLE, null, null)); + + verify(runnerConnection, timeout(5000)) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); + } + + @Test + void clearResultsIfRunnerHasNoTask() throws InterruptedException { + when(teleRunner.getCurrentTask()).thenReturn(Optional.empty()); + getResult(); + + expectClearResultsAndConsumeIt(); + } + + @Test + void clearResultsIfRunnerHasDifferentTask() throws InterruptedException { + Task task = mock(Task.class); + when(task.getId()).thenReturn(new TaskId(UUID.randomUUID())); + when(teleRunner.getCurrentTask()).thenReturn(Optional.of(task)); + getResult(); + + expectClearResultsAndConsumeIt(); + verify(runnerConnection, timeout(5000)).close(StatusCode.ILLEGAL_BEHAVIOUR); + } + + @Test + void savesResultIfRunnerHasSameTask() throws InterruptedException { + Task task = mock(Task.class); + when(task.getId()).thenReturn(new TaskId(RUN_ID)); + when(teleRunner.getCurrentTask()).thenReturn(Optional.of(task)); + GetResultReply result = getResult(); + + expectClearResultsAndConsumeIt(); + + verify(teleRunner, timeout(1000)).handleResults(result); + } + + @Test + void clearResultIfConnectionHasError() throws InterruptedException { + getStatusReplyFuture().completeExceptionally(new RuntimeException("A!")); + + expectClearResultsAndConsumeIt(); + } + + @Test + void clearResultIfSavingResultsHasError() throws InterruptedException { + doThrow(new RuntimeException("A!")).when(teleRunner).handleResults(any()); + getResult(); + + expectClearResultsAndConsumeIt(); + } + + @Test + void disconnectIfClearAfterErrorFails() throws InterruptedException { + doThrow(new RuntimeException("A!")).when(teleRunner).handleResults(any()); + doThrow(new RuntimeException("NOPE!")) + .when(runnerConnection) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.CLEAR_RESULT)); - @Test - void clearResultIfSavingResultsHasError() throws InterruptedException { - doThrow(new RuntimeException("A!")).when(teleRunner).handleResults(any()); - getResult(); + getResult(); + + verify(runnerConnection, timeout(5000)).close(StatusCode.INTERNAL_ERROR); + } - expectClearResultsAndConsumeIt(); - } + private void expectClearResultsAndConsumeIt() throws InterruptedException { + ArgumentCaptor captor; + // This is wrong, the captor accepts other classes as well + //noinspection ConstantConditions + do { + captor = ArgumentCaptor.forClass(AwaitClearResultReply.class); + verify(stateMachine, timeout(5000).atLeastOnce()).switchFromRestingState(captor.capture()); + } while (!(captor.getValue() instanceof AwaitClearResultReply)); - @Test - void disconnectIfClearAfterErrorFails() throws InterruptedException { - doThrow(new RuntimeException("A!")).when(teleRunner).handleResults(any()); - doThrow(new RuntimeException("NOPE!")).when(runnerConnection) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.CLEAR_RESULT)); + verify(runnerConnection, timeout(5000)) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.CLEAR_RESULT)); - getResult(); + captor.getValue().getReplyFuture().complete(null); + } - verify(runnerConnection, timeout(5000)).close(StatusCode.INTERNAL_ERROR); - } + private CompletableFuture getStatusReplyFuture() throws InterruptedException { + statusRequester.start(); - private void expectClearResultsAndConsumeIt() throws InterruptedException { - ArgumentCaptor captor; - // This is wrong, the captor accepts other classes as well - //noinspection ConstantConditions - do { - captor = ArgumentCaptor.forClass(AwaitClearResultReply.class); - verify(stateMachine, timeout(5000).atLeastOnce()).switchFromRestingState(captor.capture()); - } while (!(captor.getValue() instanceof AwaitClearResultReply)); + var captorStatus = ArgumentCaptor.forClass(AwaitGetStatusReply.class); + verify(stateMachine, timeout(1000)).switchFromRestingState(captorStatus.capture()); - verify(runnerConnection, timeout(5000)) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.CLEAR_RESULT)); + captorStatus + .getValue() + .getReplyFuture() + .complete(new GetStatusReply("info", "version", "version", true, Status.IDLE, null, null)); - captor.getValue().getReplyFuture().complete(null); - } + verify(runnerConnection, timeout(5000)) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); - private CompletableFuture getStatusReplyFuture() throws InterruptedException { - statusRequester.start(); + var captorResult = ArgumentCaptor.forClass(AwaitGetResultReply.class); + verify(stateMachine, timeout(1000).atLeastOnce()) + .switchFromRestingState(captorResult.capture()); - var captorStatus = ArgumentCaptor.forClass(AwaitGetStatusReply.class); - verify(stateMachine, timeout(1000)).switchFromRestingState(captorStatus.capture()); + return captorResult.getValue().getReplyFuture(); + } - captorStatus.getValue().getReplyFuture().complete(new GetStatusReply( - "info", "version", "version", true, Status.IDLE, null, null - )); + private GetResultReply getResult() throws InterruptedException { + statusRequester.start(); - verify(runnerConnection, timeout(5000)) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); + var captorStatus = ArgumentCaptor.forClass(AwaitGetStatusReply.class); + verify(stateMachine, timeout(1000)).switchFromRestingState(captorStatus.capture()); - var captorResult = ArgumentCaptor.forClass(AwaitGetResultReply.class); - verify(stateMachine, timeout(1000).atLeastOnce()) - .switchFromRestingState(captorResult.capture()); + captorStatus + .getValue() + .getReplyFuture() + .complete(new GetStatusReply("info", "version", "version", true, Status.IDLE, null, null)); - return captorResult.getValue().getReplyFuture(); - } + verify(runnerConnection, timeout(5000)) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); - private GetResultReply getResult() throws InterruptedException { - statusRequester.start(); + var captorResult = ArgumentCaptor.forClass(AwaitGetResultReply.class); + verify(stateMachine, timeout(1000).atLeastOnce()) + .switchFromRestingState(captorResult.capture()); - var captorStatus = ArgumentCaptor.forClass(AwaitGetStatusReply.class); - verify(stateMachine, timeout(1000)).switchFromRestingState(captorStatus.capture()); + GetResultReply reply = + new GetResultReply( + RUN_ID, true, mock(Result.class), null, Instant.now(), Instant.now().minusSeconds(20)); + captorResult.getValue().getReplyFuture().complete(reply); - captorStatus.getValue().getReplyFuture().complete(new GetStatusReply( - "info", "version", "version", true, Status.IDLE, null, null - )); - - verify(runnerConnection, timeout(5000)) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.GET_RESULT)); - - var captorResult = ArgumentCaptor.forClass(AwaitGetResultReply.class); - verify(stateMachine, timeout(1000).atLeastOnce()) - .switchFromRestingState(captorResult.capture()); - - GetResultReply reply = new GetResultReply( - RUN_ID, true, mock(Result.class), null, Instant.now(), Instant.now().minusSeconds(20) - ); - captorResult.getValue().getReplyFuture().complete(reply); - - return reply; - } + return reply; + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunnerTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunnerTest.java index e8696bb3f..19c95f5cb 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunnerTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/runner/single/TeleRunnerTest.java @@ -49,212 +49,218 @@ class TeleRunnerTest { - private TeleRunner runner; - private Dispatcher dispatcher; - private Queue queue; - private BenchRepo benchRepo; - - @BeforeEach - void setUp() { - dispatcher = mock(Dispatcher.class); - queue = mock(Queue.class); - benchRepo = mock(BenchRepo.class); - - when(dispatcher.getQueue()).thenReturn(queue); - this.runner = new TeleRunner( - "Runner", - new Serializer(), - dispatcher, - benchRepo - ); - } - - @Test - void registersWithDispatcherWhenReceivingInfo() { - verify(dispatcher, never()).addRunner(runner); - - runner.setRunnerInformation(mock(GetStatusReply.class)); - - verify(dispatcher).addRunner(runner); - } - - @Test - void doesNotRegisterMultipleTimes() { - verify(dispatcher, never()).addRunner(runner); - - runner.setRunnerInformation(mock(GetStatusReply.class)); - runner.setRunnerInformation(mock(GetStatusReply.class)); - - verify(dispatcher, atMostOnce()).addRunner(runner); - } - - @Test - void updatesPingWhenCreatingConnection() { - assertThat(runner.createConnection()).isNotNull(); - assertThat(ChronoUnit.MILLIS.between(runner.getLastPing(), Instant.now())).isLessThan(4000); - } - - @Test - void doesNotCreateMultipleConnections() { - assertThat(runner.createConnection()).isNotNull(); - assertThatThrownBy(() -> runner.createConnection()).isNotNull(); - - assertThat(runner.hasConnection()).isTrue(); - } - - @Test - void resetsConnectionWhenClosed() { - RunnerConnection connection = runner.createConnection(); - assertThat(connection).isNotNull(); - - connection.close(StatusCode.INTERNAL_ERROR); - connection.onWebSocketClose(200, ""); - - assertThat(runner.hasConnection()).isFalse(); - } - - @Test - void disposeReleasesResources() { - assertThat(runner.createConnection()).isNotNull(); - assertThat(runner.hasConnection()).isTrue(); - - runner.dispose(); - - assertThat(runner.hasConnection()).isTrue(); - assertThat(runner.isDisposed()).isTrue(); - assertThatThrownBy(() -> runner.createConnection()).isInstanceOf(IllegalStateException.class); - } - - @Test - void updatesRunnerInformation() { - GetStatusReply first = new GetStatusReply( - "hey", "there", "my", false, Status.IDLE, null, null - ); - runner.setRunnerInformation(first); - - assertThat(runner.getRunnerInformation()).isEqualTo(new KnownRunner( - runner.getRunnerName(), "hey", "there", Status.IDLE, null, true, null, null, List.of())); - - GetStatusReply second = new GetStatusReply( - "hey2", "there2", "my2", false, Status.IDLE, null, null - ); - runner.setRunnerInformation(second); - - assertThat(runner.getRunnerInformation()).isEqualTo(new KnownRunner( - runner.getRunnerName(), "hey2", "there2", Status.IDLE, null, true, null, null, List.of())); - } - - // FIXME: This is questionable below here - - @Test - void discardsResultIfItHadNoTask() { - GetResultReply resultReply = new GetResultReply( - UUID.randomUUID(), true, mock(Result.class), null, Instant.now(), Instant.now() - ); - runner.handleResults(resultReply); - - verify(queue).abortTask(new TaskId(resultReply.getRunId())); - } - - @Test - void passesSuccessResultOn() throws NoSuchFieldException, IllegalAccessException { - Result result = new Result( - List.of(new Benchmark( - "test", List.of(new Metric("hello", "argh", null, null, null)) - )), null - ); - UUID runId = UUID.randomUUID(); - NewRun run = handleResult(runId, result); - - assertThat(run.getId()).isEqualTo(new RunId(runId)); - assertThat(run.getResult().getRight()).isPresent(); - } - - @Test - void passesFailedResultOn() throws NoSuchFieldException, IllegalAccessException { - Result result = new Result( - null, "I am a big error" - ); - UUID runId = UUID.randomUUID(); - NewRun run = handleResult(runId, result); - - assertThat(run.getId()).isEqualTo(new RunId(runId)); - assertThat(run.getResult().getLeft()) - .get() - .extracting(RunError::getType) - .isEqualTo(RunErrorType.BENCH_SCRIPT_ERROR); - assertThat(run.getResult().getLeft()) - .get() - .extracting(RunError::getMessage) - .isEqualTo(result.getError().orElseThrow()); - } - - // FIXME: This is really questionable below here - - @Test - void sendsWork() throws NoSuchFieldException, IllegalAccessException { - Task task = buildTask(UUID.randomUUID()); - setTask(task); - - RunnerConnection connection = mock(RunnerConnection.class); - @SuppressWarnings("unchecked") - StateMachine stateMachine = mock(StateMachine.class); - when(connection.getStateMachine()).thenReturn(stateMachine); - - setConnection(connection); - runner.setRunnerInformation(new GetStatusReply( - "Hello", "my version", "currentHash", false, Status.IDLE, null, null - )); - when(benchRepo.getCurrentHash()).thenReturn(Optional.of(new CommitHash("otherHash"))); - - runner.prepareAndSendWork(); - verify(connection, times(2)).createBinaryOutputStream(); - verify(connection) - .send(argThat(argument -> argument.getType() == ClientBoundPacketType.REQUEST_RUN_REPLY)); - } - - private NewRun handleResult(UUID runId, Result result) - throws NoSuchFieldException, IllegalAccessException { - GetResultReply resultReply = new GetResultReply( - runId, true, result, null, Instant.now(), Instant.now() - ); - Task task = buildTask(runId); - setTask(task); - - runner.setRunnerInformation(new GetStatusReply( - "hey", "there", null, false, Status.IDLE, null, null - )); - runner.handleResults(resultReply); - - ArgumentCaptor captor = ArgumentCaptor.forClass(NewRun.class); - verify(dispatcher).completeTask(captor.capture()); - - return captor.getValue(); - } - - private Task buildTask(UUID runId) { - return new Task( - new TaskId(runId), "Peter", TaskPriority.LISTENER, - Instant.now(), Instant.now(), - Either.ofLeft(new CommitSource( - new RepoId(UUID.randomUUID()), new CommitHash("hello") - )), - false - ); - } - - private void setTask(Task task) throws NoSuchFieldException, IllegalAccessException { - Field myCurrentTask = runner.getClass().getDeclaredField("myCurrentTask"); - myCurrentTask.setAccessible(true); - //noinspection unchecked - AtomicReference reference = (AtomicReference) myCurrentTask.get(runner); - reference.set(task); - } - - private void setConnection(RunnerConnection connection) - throws NoSuchFieldException, IllegalAccessException { - Field myCurrentTask = runner.getClass().getDeclaredField("connection"); - myCurrentTask.setAccessible(true); - myCurrentTask.set(runner, connection); - } + private TeleRunner runner; + private Dispatcher dispatcher; + private Queue queue; + private BenchRepo benchRepo; + + @BeforeEach + void setUp() { + dispatcher = mock(Dispatcher.class); + queue = mock(Queue.class); + benchRepo = mock(BenchRepo.class); + + when(dispatcher.getQueue()).thenReturn(queue); + this.runner = new TeleRunner("Runner", new Serializer(), dispatcher, benchRepo); + } + + @Test + void registersWithDispatcherWhenReceivingInfo() { + verify(dispatcher, never()).addRunner(runner); + + runner.setRunnerInformation(mock(GetStatusReply.class)); + + verify(dispatcher).addRunner(runner); + } + + @Test + void doesNotRegisterMultipleTimes() { + verify(dispatcher, never()).addRunner(runner); + + runner.setRunnerInformation(mock(GetStatusReply.class)); + runner.setRunnerInformation(mock(GetStatusReply.class)); + + verify(dispatcher, atMostOnce()).addRunner(runner); + } + + @Test + void updatesPingWhenCreatingConnection() { + assertThat(runner.createConnection()).isNotNull(); + assertThat(ChronoUnit.MILLIS.between(runner.getLastPing(), Instant.now())).isLessThan(4000); + } + + @Test + void doesNotCreateMultipleConnections() { + assertThat(runner.createConnection()).isNotNull(); + assertThatThrownBy(() -> runner.createConnection()).isNotNull(); + + assertThat(runner.hasConnection()).isTrue(); + } + + @Test + void resetsConnectionWhenClosed() { + RunnerConnection connection = runner.createConnection(); + assertThat(connection).isNotNull(); + + connection.close(StatusCode.INTERNAL_ERROR); + connection.onWebSocketClose(200, ""); + + assertThat(runner.hasConnection()).isFalse(); + } + + @Test + void disposeReleasesResources() { + assertThat(runner.createConnection()).isNotNull(); + assertThat(runner.hasConnection()).isTrue(); + + runner.dispose(); + + assertThat(runner.hasConnection()).isTrue(); + assertThat(runner.isDisposed()).isTrue(); + assertThatThrownBy(() -> runner.createConnection()).isInstanceOf(IllegalStateException.class); + } + + @Test + void updatesRunnerInformation() { + GetStatusReply first = new GetStatusReply("hey", "there", "my", false, Status.IDLE, null, null); + runner.setRunnerInformation(first); + + assertThat(runner.getRunnerInformation()) + .isEqualTo( + new KnownRunner( + runner.getRunnerName(), + "hey", + "there", + Status.IDLE, + null, + true, + null, + null, + List.of())); + + GetStatusReply second = + new GetStatusReply("hey2", "there2", "my2", false, Status.IDLE, null, null); + runner.setRunnerInformation(second); + + assertThat(runner.getRunnerInformation()) + .isEqualTo( + new KnownRunner( + runner.getRunnerName(), + "hey2", + "there2", + Status.IDLE, + null, + true, + null, + null, + List.of())); + } + + // FIXME: This is questionable below here + + @Test + void discardsResultIfItHadNoTask() { + GetResultReply resultReply = + new GetResultReply( + UUID.randomUUID(), true, mock(Result.class), null, Instant.now(), Instant.now()); + runner.handleResults(resultReply); + + verify(queue).abortTask(new TaskId(resultReply.getRunId())); + } + + @Test + void passesSuccessResultOn() throws NoSuchFieldException, IllegalAccessException { + Result result = + new Result( + List.of(new Benchmark("test", List.of(new Metric("hello", "argh", null, null, null)))), + null); + UUID runId = UUID.randomUUID(); + NewRun run = handleResult(runId, result); + + assertThat(run.getId()).isEqualTo(new RunId(runId)); + assertThat(run.getResult().getRight()).isPresent(); + } + + @Test + void passesFailedResultOn() throws NoSuchFieldException, IllegalAccessException { + Result result = new Result(null, "I am a big error"); + UUID runId = UUID.randomUUID(); + NewRun run = handleResult(runId, result); + + assertThat(run.getId()).isEqualTo(new RunId(runId)); + assertThat(run.getResult().getLeft()) + .get() + .extracting(RunError::getType) + .isEqualTo(RunErrorType.BENCH_SCRIPT_ERROR); + assertThat(run.getResult().getLeft()) + .get() + .extracting(RunError::getMessage) + .isEqualTo(result.getError().orElseThrow()); + } + + // FIXME: This is really questionable below here + + @Test + void sendsWork() throws NoSuchFieldException, IllegalAccessException { + Task task = buildTask(UUID.randomUUID()); + setTask(task); + + RunnerConnection connection = mock(RunnerConnection.class); + @SuppressWarnings("unchecked") + StateMachine stateMachine = mock(StateMachine.class); + when(connection.getStateMachine()).thenReturn(stateMachine); + + setConnection(connection); + runner.setRunnerInformation( + new GetStatusReply("Hello", "my version", "currentHash", false, Status.IDLE, null, null)); + when(benchRepo.getCurrentHash()).thenReturn(Optional.of(new CommitHash("otherHash"))); + + runner.prepareAndSendWork(); + verify(connection, times(2)).createBinaryOutputStream(); + verify(connection) + .send(argThat(argument -> argument.getType() == ClientBoundPacketType.REQUEST_RUN_REPLY)); + } + + private NewRun handleResult(UUID runId, Result result) + throws NoSuchFieldException, IllegalAccessException { + GetResultReply resultReply = + new GetResultReply(runId, true, result, null, Instant.now(), Instant.now()); + Task task = buildTask(runId); + setTask(task); + + runner.setRunnerInformation( + new GetStatusReply("hey", "there", null, false, Status.IDLE, null, null)); + runner.handleResults(resultReply); + + ArgumentCaptor captor = ArgumentCaptor.forClass(NewRun.class); + verify(dispatcher).completeTask(captor.capture()); + + return captor.getValue(); + } + + private Task buildTask(UUID runId) { + return new Task( + new TaskId(runId), + "Peter", + TaskPriority.LISTENER, + Instant.now(), + Instant.now(), + Either.ofLeft(new CommitSource(new RepoId(UUID.randomUUID()), new CommitHash("hello"))), + false); + } + + private void setTask(Task task) throws NoSuchFieldException, IllegalAccessException { + Field myCurrentTask = runner.getClass().getDeclaredField("myCurrentTask"); + myCurrentTask.setAccessible(true); + //noinspection unchecked + AtomicReference reference = (AtomicReference) myCurrentTask.get(runner); + reference.set(task); + } + + private void setConnection(RunnerConnection connection) + throws NoSuchFieldException, IllegalAccessException { + Field myCurrentTask = runner.getClass().getDeclaredField("connection"); + myCurrentTask.setAccessible(true); + myCurrentTask.set(runner, connection); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorageTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorageTest.java index 49e1669e7..6026a3675 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorageTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/db/DatabaseStorageTest.java @@ -15,104 +15,111 @@ class DatabaseStorageTest { - private TestDb testDb; - - @BeforeEach - void setUp(@TempDir Path tempDir) { - testDb = new TestDb(tempDir); - } - - @Test - void canReadExistingInfo(@TempDir Path tempDir) { - String repoId = new RepoId().getIdAsString(); - String repoName = "test repo"; - String repoRemoteUrl = "https://github.com/IPDSnelting/velcom.git"; - - testDb.db().batchInsert(new RepoRecord(repoId, repoName, repoRemoteUrl, null, null)).execute(); - DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - - // Read without explicit transaction - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - RepoRecord record = db.dsl().selectFrom(REPO).fetchSingle(); - assertThat(record.getId()).isEqualTo(repoId); - assertThat(record.getName()).isEqualTo(repoName); - assertThat(record.getRemoteUrl()).isEqualTo(repoRemoteUrl); - } - - // Read transaction, no return value - databaseStorage.acquireReadTransaction(db -> { - RepoRecord record = db.dsl().selectFrom(REPO).fetchSingle(); - assertThat(record.getId()).isEqualTo(repoId); - assertThat(record.getName()).isEqualTo(repoName); - assertThat(record.getRemoteUrl()).isEqualTo(repoRemoteUrl); - }); - - // Read transaction with return value - RepoRecord record = databaseStorage.acquireReadTransaction(db -> { - return db.dsl().selectFrom(REPO).fetchSingle(); - }); - assertThat(record.getId()).isEqualTo(repoId); - assertThat(record.getName()).isEqualTo(repoName); - assertThat(record.getRemoteUrl()).isEqualTo(repoRemoteUrl); - } - - @Test - void canWriteNewInfo(@TempDir Path tempDir) { - String repoId1 = new RepoId().getIdAsString(); - String repoId2 = new RepoId().getIdAsString(); - String repoId3 = new RepoId().getIdAsString(); - String repoName1 = "test repo 1"; - String repoName2 = "test repo 2"; - String repoName3 = "test repo 3"; - String repoRemoteUrl = "https://github.com/IPDSnelting/velcom.git"; - - TestDb testDb = new TestDb(tempDir); - DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - - // Write without explicit transaction - try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { - db.dsl().batchInsert(new RepoRecord(repoId1, repoName1, repoRemoteUrl, null, null)).execute(); - } - - // Write transaction, no return value - databaseStorage.acquireWriteTransaction(db -> { - db.dsl().batchInsert(new RepoRecord(repoId2, repoName2, repoRemoteUrl, null, null)).execute(); - }); - - // Write transaction with return value - databaseStorage.acquireWriteTransaction(db -> { - db.dsl().batchInsert(new RepoRecord(repoId3, repoName3, repoRemoteUrl, null, null)).execute(); - return true; - }); - - // Check if all three repos have been added correctly - try (DBReadAccess db = databaseStorage.acquireReadAccess()) { - Map records = db.dsl() - .selectFrom(REPO) - .stream() - .collect(toMap(RepoRecord::getId, it -> it)); - - assertThat(records).containsOnlyKeys(repoId1, repoId2, repoId3); - assertThat(records.get(repoId1).getName()).isEqualTo(repoName1); - assertThat(records.get(repoId2).getName()).isEqualTo(repoName2); - assertThat(records.get(repoId3).getName()).isEqualTo(repoName3); - } - } - - @Test - void transactionsPassThroughReturnValue() { - DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); - - boolean returned; - - returned = databaseStorage.acquireReadTransaction(db -> true); - assertThat(returned).isTrue(); - returned = databaseStorage.acquireReadTransaction(db -> false); - assertThat(returned).isFalse(); - - returned = databaseStorage.acquireWriteTransaction(db -> true); - assertThat(returned).isTrue(); - returned = databaseStorage.acquireWriteTransaction(db -> false); - assertThat(returned).isFalse(); - } + private TestDb testDb; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + testDb = new TestDb(tempDir); + } + + @Test + void canReadExistingInfo(@TempDir Path tempDir) { + String repoId = new RepoId().getIdAsString(); + String repoName = "test repo"; + String repoRemoteUrl = "https://github.com/IPDSnelting/velcom.git"; + + testDb.db().batchInsert(new RepoRecord(repoId, repoName, repoRemoteUrl, null, null)).execute(); + DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + + // Read without explicit transaction + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + RepoRecord record = db.dsl().selectFrom(REPO).fetchSingle(); + assertThat(record.getId()).isEqualTo(repoId); + assertThat(record.getName()).isEqualTo(repoName); + assertThat(record.getRemoteUrl()).isEqualTo(repoRemoteUrl); + } + + // Read transaction, no return value + databaseStorage.acquireReadTransaction( + db -> { + RepoRecord record = db.dsl().selectFrom(REPO).fetchSingle(); + assertThat(record.getId()).isEqualTo(repoId); + assertThat(record.getName()).isEqualTo(repoName); + assertThat(record.getRemoteUrl()).isEqualTo(repoRemoteUrl); + }); + + // Read transaction with return value + RepoRecord record = + databaseStorage.acquireReadTransaction( + db -> { + return db.dsl().selectFrom(REPO).fetchSingle(); + }); + assertThat(record.getId()).isEqualTo(repoId); + assertThat(record.getName()).isEqualTo(repoName); + assertThat(record.getRemoteUrl()).isEqualTo(repoRemoteUrl); + } + + @Test + void canWriteNewInfo(@TempDir Path tempDir) { + String repoId1 = new RepoId().getIdAsString(); + String repoId2 = new RepoId().getIdAsString(); + String repoId3 = new RepoId().getIdAsString(); + String repoName1 = "test repo 1"; + String repoName2 = "test repo 2"; + String repoName3 = "test repo 3"; + String repoRemoteUrl = "https://github.com/IPDSnelting/velcom.git"; + + TestDb testDb = new TestDb(tempDir); + DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + + // Write without explicit transaction + try (DBWriteAccess db = databaseStorage.acquireWriteAccess()) { + db.dsl().batchInsert(new RepoRecord(repoId1, repoName1, repoRemoteUrl, null, null)).execute(); + } + + // Write transaction, no return value + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .batchInsert(new RepoRecord(repoId2, repoName2, repoRemoteUrl, null, null)) + .execute(); + }); + + // Write transaction with return value + databaseStorage.acquireWriteTransaction( + db -> { + db.dsl() + .batchInsert(new RepoRecord(repoId3, repoName3, repoRemoteUrl, null, null)) + .execute(); + return true; + }); + + // Check if all three repos have been added correctly + try (DBReadAccess db = databaseStorage.acquireReadAccess()) { + Map records = + db.dsl().selectFrom(REPO).stream().collect(toMap(RepoRecord::getId, it -> it)); + + assertThat(records).containsOnlyKeys(repoId1, repoId2, repoId3); + assertThat(records.get(repoId1).getName()).isEqualTo(repoName1); + assertThat(records.get(repoId2).getName()).isEqualTo(repoName2); + assertThat(records.get(repoId3).getName()).isEqualTo(repoName3); + } + } + + @Test + void transactionsPassThroughReturnValue() { + DatabaseStorage databaseStorage = new DatabaseStorage(testDb.closeAndGetJdbcUrl()); + + boolean returned; + + returned = databaseStorage.acquireReadTransaction(db -> true); + assertThat(returned).isTrue(); + returned = databaseStorage.acquireReadTransaction(db -> false); + assertThat(returned).isFalse(); + + returned = databaseStorage.acquireWriteTransaction(db -> true); + assertThat(returned).isTrue(); + returned = databaseStorage.acquireWriteTransaction(db -> false); + assertThat(returned).isFalse(); + } } diff --git a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorageTest.java b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorageTest.java index a57f729c8..e9812764b 100644 --- a/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorageTest.java +++ b/backend/backend/src/test/java/de/aaaaaaah/velcom/backend/storage/repo/RepoStorageTest.java @@ -26,165 +26,162 @@ class RepoStorageTest { - static final List commits = List.of( - new TestCommit("Add abc", "abc.txt", "Hello World!"), - new TestCommit("Add blablub", "blablub.txt", "123123123"), - new TestCommit("Modify abc", "abc.txt", "Hello World! Test 123") - ); - static final TestCommit headTestCommit = commits.get(commits.size() - 1); - - @TempDir - Path testDir; - Path repoStorageDir; - Path testRepoDir; - - String cloneUrl; - String dirName = "tiny_repo"; - - RepoStorage repoStorage; - - @BeforeEach - void setUp() throws IOException, GitAPIException { - this.repoStorageDir = testDir.resolve("repos"); - this.testRepoDir = testDir.resolve("custom_test_repo"); - this.repoStorage = new RepoStorage(repoStorageDir); - this.cloneUrl = "file://" + testRepoDir.toString(); - - new TestRepo(testRepoDir, commits); - } - - @Test - void testAddRepository() throws AddRepositoryException, IOException { - Path tinyRepoPath = repoStorage.addRepository(dirName, cloneUrl); - - assertTrue(Files.exists(tinyRepoPath)); - assertTrue(Files.isDirectory(tinyRepoPath)); - assertTrue(repoStorage.containsRepository(dirName)); - assertEquals(dirName, tinyRepoPath.getFileName().toString()); - - // Make sure testRepoPath is actually a repository - try (Git git = Git.open(tinyRepoPath.toFile())) { - Repository repo = git.getRepository(); - - final ObjectId headPtr = repo.resolve("HEAD"); - - // And that at least the head commit exists and seems right - try (RevWalk walk = new RevWalk(repo)) { - final RevCommit commit = walk.parseCommit(headPtr); + static final List commits = + List.of( + new TestCommit("Add abc", "abc.txt", "Hello World!"), + new TestCommit("Add blablub", "blablub.txt", "123123123"), + new TestCommit("Modify abc", "abc.txt", "Hello World! Test 123")); + static final TestCommit headTestCommit = commits.get(commits.size() - 1); - assertEquals(headTestCommit.getMessage(), commit.getFullMessage()); - } - } - } + @TempDir Path testDir; + Path repoStorageDir; + Path testRepoDir; - @Test - void testContainsAndDelete() throws AddRepositoryException, IOException { - assertFalse(repoStorage.containsRepository(dirName)); + String cloneUrl; + String dirName = "tiny_repo"; - Path path = repoStorage.addRepository(dirName, cloneUrl); - - assertEquals(repoStorageDir.resolve(dirName), path); - assertTrue(Files.exists(path)); - assertTrue(Files.isDirectory(path)); - assertTrue(repoStorage.containsRepository(dirName)); - - repoStorage.deleteRepository(dirName); - - assertTrue(Files.notExists(path)); - assertFalse(repoStorage.containsRepository(dirName)); - } - - @Test - void testGetRepoDirectories() throws AddRepositoryException, IOException { - assertTrue(repoStorage.getRepoDirectories().isEmpty()); - repoStorage.addRepository(dirName, cloneUrl); - assertEquals(repoStorage.getRepoDirectories().size(), 1); - - repoStorage.addRepository(dirName + "_2", cloneUrl); - - assertEquals(repoStorage.getRepoDirectories().size(), 2); - assertTrue( - repoStorage.getRepoDirectories().stream() - .anyMatch(p -> p.getFileName().endsWith(dirName)) - ); - assertTrue( - repoStorage.getRepoDirectories().stream() - .anyMatch(p -> p.getFileName().endsWith(dirName + "_2")) - ); - - repoStorage.deleteRepository(dirName); - - assertEquals(repoStorage.getRepoDirectories().size(), 1); - assertFalse( - repoStorage.getRepoDirectories().stream() - .anyMatch(p -> p.getFileName().endsWith(dirName)) - ); - assertTrue( - repoStorage.getRepoDirectories().stream() - .anyMatch(p -> p.getFileName().endsWith(dirName + "_2")) - ); - - repoStorage.deleteRepository(dirName + "_2"); - - assertTrue(repoStorage.getRepoDirectories().isEmpty()); - } - - @Test - void testGetRepoDir() throws AddRepositoryException, IOException { - assertThrows(NoSuchRepositoryException.class, () -> repoStorage.getRepoDir(dirName)); - repoStorage.addRepository(dirName, cloneUrl); + RepoStorage repoStorage; - Path repoDir = repoStorage.getRepoDir(dirName); - assertEquals(repoStorageDir.resolve(dirName), repoDir); - assertTrue(Files.exists(repoDir)); - assertTrue(Files.isDirectory(repoDir)); + @BeforeEach + void setUp() throws IOException, GitAPIException { + this.repoStorageDir = testDir.resolve("repos"); + this.testRepoDir = testDir.resolve("custom_test_repo"); + this.repoStorage = new RepoStorage(repoStorageDir); + this.cloneUrl = "file://" + testRepoDir.toString(); - repoStorage.deleteRepository(dirName); - assertThrows(NoSuchRepositoryException.class, () -> repoStorage.getRepoDir(dirName)); - } + new TestRepo(testRepoDir, commits); + } - @Test - public void testAcquire() - throws AddRepositoryException, RepositoryAcquisitionException, IOException { + @Test + void testAddRepository() throws AddRepositoryException, IOException { + Path tinyRepoPath = repoStorage.addRepository(dirName, cloneUrl); - repoStorage.addRepository(dirName, cloneUrl); + assertTrue(Files.exists(tinyRepoPath)); + assertTrue(Files.isDirectory(tinyRepoPath)); + assertTrue(repoStorage.containsRepository(dirName)); + assertEquals(dirName, tinyRepoPath.getFileName().toString()); - try (Repository repo = repoStorage.acquireRepository(dirName)) { - final ObjectId headPtr = repo.resolve("HEAD"); + // Make sure testRepoPath is actually a repository + try (Git git = Git.open(tinyRepoPath.toFile())) { + Repository repo = git.getRepository(); - try (RevWalk walk = new RevWalk(repo)) { - final RevCommit commit = walk.parseCommit(headPtr); + final ObjectId headPtr = repo.resolve("HEAD"); - assertEquals(headTestCommit.getMessage(), commit.getFullMessage()); - } - } + // And that at least the head commit exists and seems right + try (RevWalk walk = new RevWalk(repo)) { + final RevCommit commit = walk.parseCommit(headPtr); - repoStorage.deleteRepository(dirName); - assertThrows(NoSuchRepositoryException.class, () -> repoStorage.acquireRepository(dirName)); - } + assertEquals(headTestCommit.getMessage(), commit.getFullMessage()); + } + } + } - @Test - public void testAcquireWithLambda() - throws AddRepositoryException, RepositoryAcquisitionException, IOException { + @Test + void testContainsAndDelete() throws AddRepositoryException, IOException { + assertFalse(repoStorage.containsRepository(dirName)); - repoStorage.addRepository(dirName, cloneUrl); - - repoStorage.acquireRepository(dirName, repo -> { - final ObjectId headPtr = repo.resolve("HEAD"); + Path path = repoStorage.addRepository(dirName, cloneUrl); + + assertEquals(repoStorageDir.resolve(dirName), path); + assertTrue(Files.exists(path)); + assertTrue(Files.isDirectory(path)); + assertTrue(repoStorage.containsRepository(dirName)); + + repoStorage.deleteRepository(dirName); + + assertTrue(Files.notExists(path)); + assertFalse(repoStorage.containsRepository(dirName)); + } + + @Test + void testGetRepoDirectories() throws AddRepositoryException, IOException { + assertTrue(repoStorage.getRepoDirectories().isEmpty()); + repoStorage.addRepository(dirName, cloneUrl); + assertEquals(repoStorage.getRepoDirectories().size(), 1); + + repoStorage.addRepository(dirName + "_2", cloneUrl); + + assertEquals(repoStorage.getRepoDirectories().size(), 2); + assertTrue( + repoStorage.getRepoDirectories().stream().anyMatch(p -> p.getFileName().endsWith(dirName))); + assertTrue( + repoStorage.getRepoDirectories().stream() + .anyMatch(p -> p.getFileName().endsWith(dirName + "_2"))); + + repoStorage.deleteRepository(dirName); + + assertEquals(repoStorage.getRepoDirectories().size(), 1); + assertFalse( + repoStorage.getRepoDirectories().stream().anyMatch(p -> p.getFileName().endsWith(dirName))); + assertTrue( + repoStorage.getRepoDirectories().stream() + .anyMatch(p -> p.getFileName().endsWith(dirName + "_2"))); + + repoStorage.deleteRepository(dirName + "_2"); + + assertTrue(repoStorage.getRepoDirectories().isEmpty()); + } + + @Test + void testGetRepoDir() throws AddRepositoryException, IOException { + assertThrows(NoSuchRepositoryException.class, () -> repoStorage.getRepoDir(dirName)); + repoStorage.addRepository(dirName, cloneUrl); + + Path repoDir = repoStorage.getRepoDir(dirName); + assertEquals(repoStorageDir.resolve(dirName), repoDir); + assertTrue(Files.exists(repoDir)); + assertTrue(Files.isDirectory(repoDir)); - try (RevWalk walk = new RevWalk(repo)) { - final RevCommit commit = walk.parseCommit(headPtr); + repoStorage.deleteRepository(dirName); + assertThrows(NoSuchRepositoryException.class, () -> repoStorage.getRepoDir(dirName)); + } - assertEquals(headTestCommit.getMessage(), commit.getFullMessage()); - } - }); + @Test + public void testAcquire() + throws AddRepositoryException, RepositoryAcquisitionException, IOException { - repoStorage.deleteRepository(dirName); - assertThrows(NoSuchRepositoryException.class, - () -> repoStorage.acquireRepository(dirName, repo -> { - throw new AssertionError(); - }) - ); - } + repoStorage.addRepository(dirName, cloneUrl); + + try (Repository repo = repoStorage.acquireRepository(dirName)) { + final ObjectId headPtr = repo.resolve("HEAD"); + + try (RevWalk walk = new RevWalk(repo)) { + final RevCommit commit = walk.parseCommit(headPtr); -} \ No newline at end of file + assertEquals(headTestCommit.getMessage(), commit.getFullMessage()); + } + } + + repoStorage.deleteRepository(dirName); + assertThrows(NoSuchRepositoryException.class, () -> repoStorage.acquireRepository(dirName)); + } + + @Test + public void testAcquireWithLambda() + throws AddRepositoryException, RepositoryAcquisitionException, IOException { + + repoStorage.addRepository(dirName, cloneUrl); + + repoStorage.acquireRepository( + dirName, + repo -> { + final ObjectId headPtr = repo.resolve("HEAD"); + + try (RevWalk walk = new RevWalk(repo)) { + final RevCommit commit = walk.parseCommit(headPtr); + + assertEquals(headTestCommit.getMessage(), commit.getFullMessage()); + } + }); + + repoStorage.deleteRepository(dirName); + assertThrows( + NoSuchRepositoryException.class, + () -> + repoStorage.acquireRepository( + dirName, + repo -> { + throw new AssertionError(); + })); + } +} diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Connection.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Connection.java index 21f362c04..4d64b638b 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Connection.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Connection.java @@ -21,226 +21,225 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A web socket connection to a backend. - */ +/** A web socket connection to a backend. */ public class Connection implements WebSocket.Listener, HeartbeatWebsocket { - private static final Logger LOGGER = LoggerFactory.getLogger(Connection.class); - - private final StateMachine stateMachine; - private final Serializer serializer; - private StringBuilder textPacketBuilder; - private final CompletableFuture closedFuture; - private boolean closed; - - // Initialized in #onOpen(). Should be initialized once constructor completes. - private WebSocket socket; - private HeartbeatHandler heartbeatHandler; - - public Connection(TeleBackend teleBackend, HttpClient httpClient, URI address, String name, - String token) throws ExecutionException, InterruptedException { - - stateMachine = new StateMachine<>(new Idle(teleBackend, this)); - serializer = new Serializer(); - textPacketBuilder = new StringBuilder(); - closedFuture = new CompletableFuture<>(); - closed = false; - - LOGGER.debug("Opening connection to {}", address); - httpClient.newWebSocketBuilder() - .header(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName(), name) - .header(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName(), token) - .buildAsync(address, this) - .get(); - // At this point, #onOpen() should have been called already, so the socket and heartbeatHandler - // have been initialized. - LOGGER.debug("Successfully opened connection to {}", address); - } - - /** - * Send a packet to the server. - * - * @param packet the packet to send to the server - */ - public synchronized void sendPacket(ServerBoundPacket packet) { - if (closed) { - return; - } - - serializer.serialize(packet).ifPresentOrElse( - str -> socket.sendText(str, true), - () -> { - LOGGER.warn("Failed to serialize and send packet, closing connection"); - LOGGER.warn("Packet: {}", packet); - close(StatusCode.ILLEGAL_PACKET); - } - ); - } - - /** - * Do whatever needs to do when the connection transitions into the closed state. This function is - * threadsafe and can be called multiple times. - */ - private synchronized void cleanupAfterClosed() { - if (closed) { - return; - } - closed = true; - - LOGGER.debug("Cleaning up after closing the connection"); - stateMachine.stop(); - closedFuture.complete(null); - heartbeatHandler.shutdown(); - } - - /** - * Close the connection gracefully. Uses {@link #forceClose(StatusCode)} if the connection is not - * closed within the {@link Delays#CLOSE_CONNECTION_TIMEOUT}. - * - * @param statusCode the status code to close the connection with - */ - public synchronized void close(StatusCode statusCode) { - if (closed) { - return; - } - - LOGGER.warn("Closing connection: {}", statusCode.getDescription()); - socket.sendClose(statusCode.getCode(), statusCode.getDescriptionAsReason()); - - Timeout disconnectTimeout = Timeout.after(Delays.CLOSE_CONNECTION_TIMEOUT); - disconnectTimeout.getCompletionStage().thenAccept(aVoid -> forceClose(statusCode)); - disconnectTimeout.start(); - } - - - /** - * Forcefully close the connection immediately. - * - * @param statusCode the status code to close the connection with - */ - public synchronized void forceClose(StatusCode statusCode) { - if (closed) { - return; - } - - LOGGER.warn("Force-closing connection: {}", statusCode.getDescription()); - socket.abort(); - - // Since neither onClose nor onError are called when the socket is aborted like this, we - // need to call cleanupAfterClosed here too. - cleanupAfterClosed(); - } - - /** - * Allow outside functions to switch the state machine state. Waits for a resting state and then - * switches to the specified state. - * - * @param state the state to switch to - * @return whether the state was switched successfully - * @throws InterruptedException if the thread got interrupted while waiting for the state machine - * to enter a resting state - */ - public boolean switchFromRestingState(RunnerState state) throws InterruptedException { - return stateMachine.switchFromRestingState(state); - } - - public Serializer getSerializer() { - return serializer; - } - - public Future getClosedFuture() { - return closedFuture; - } - - @Override - public void onOpen(WebSocket webSocket) { - webSocket.request(1); - - // Initial call by the websocket library, so we can use it to obtain our socket for socket-y - // purposes. - synchronized (this) { - socket = webSocket; - heartbeatHandler = new HeartbeatHandler(this); - } - } - - @Override - public synchronized CompletionStage onText(WebSocket webSocket, CharSequence data, - boolean last) { - - webSocket.request(1); - if (closed) { - return null; - } - - textPacketBuilder.append(data); - - if (last) { - String text = textPacketBuilder.toString(); - textPacketBuilder = new StringBuilder(); - stateMachine.changeCurrentState(state -> state.onText(text)); - } - - return null; - } - - @Override - public synchronized CompletionStage onBinary(WebSocket webSocket, ByteBuffer data, - boolean last) { - - webSocket.request(1); - if (closed) { - return null; - } - - stateMachine.changeCurrentState(state -> state.onBinary(data, last)); - - return null; - } - - @Override - public synchronized CompletionStage onClose(WebSocket webSocket, int statusCode, - String reason) { - - String statusCodeStr = StatusCode.fromCode(statusCode) - .map(Enum::toString) - .orElse(Integer.toString(statusCode)); - LOGGER.debug("Connection closed normally, status code: {}", statusCodeStr); - cleanupAfterClosed(); - return null; - } - - @Override - public synchronized void onError(WebSocket webSocket, Throwable error) { - // For some reason, this function is not called after a socket.abort(). - LOGGER.warn("Connection closed abnormally"); - cleanupAfterClosed(); - } - - @Override - public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { - webSocket.request(1); - heartbeatHandler.onPong(); - return null; - } - - @Override - public void onTimeoutDetected() { - LOGGER.warn("Detected ping timeout, disconnecting"); - close(StatusCode.PING_TIMEOUT); - } - - @Override - public boolean sendPing() { - synchronized (this) { - if (closed) { - LOGGER.debug("Couldn't send ping because connection already closed"); - return false; - } - - socket.sendPing(ByteBuffer.wrap(Long.toString(System.currentTimeMillis()).getBytes())); - return true; - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(Connection.class); + + private final StateMachine stateMachine; + private final Serializer serializer; + private StringBuilder textPacketBuilder; + private final CompletableFuture closedFuture; + private boolean closed; + + // Initialized in #onOpen(). Should be initialized once constructor completes. + private WebSocket socket; + private HeartbeatHandler heartbeatHandler; + + public Connection( + TeleBackend teleBackend, HttpClient httpClient, URI address, String name, String token) + throws ExecutionException, InterruptedException { + + stateMachine = new StateMachine<>(new Idle(teleBackend, this)); + serializer = new Serializer(); + textPacketBuilder = new StringBuilder(); + closedFuture = new CompletableFuture<>(); + closed = false; + + LOGGER.debug("Opening connection to {}", address); + httpClient + .newWebSocketBuilder() + .header(RunnerConnectionHeader.CONNECT_RUNNER_NAME.getName(), name) + .header(RunnerConnectionHeader.CONNECT_RUNNER_TOKEN.getName(), token) + .buildAsync(address, this) + .get(); + // At this point, #onOpen() should have been called already, so the socket and heartbeatHandler + // have been initialized. + LOGGER.debug("Successfully opened connection to {}", address); + } + + /** + * Send a packet to the server. + * + * @param packet the packet to send to the server + */ + public synchronized void sendPacket(ServerBoundPacket packet) { + if (closed) { + return; + } + + serializer + .serialize(packet) + .ifPresentOrElse( + str -> socket.sendText(str, true), + () -> { + LOGGER.warn("Failed to serialize and send packet, closing connection"); + LOGGER.warn("Packet: {}", packet); + close(StatusCode.ILLEGAL_PACKET); + }); + } + + /** + * Do whatever needs to do when the connection transitions into the closed state. This function is + * threadsafe and can be called multiple times. + */ + private synchronized void cleanupAfterClosed() { + if (closed) { + return; + } + closed = true; + + LOGGER.debug("Cleaning up after closing the connection"); + stateMachine.stop(); + closedFuture.complete(null); + heartbeatHandler.shutdown(); + } + + /** + * Close the connection gracefully. Uses {@link #forceClose(StatusCode)} if the connection is not + * closed within the {@link Delays#CLOSE_CONNECTION_TIMEOUT}. + * + * @param statusCode the status code to close the connection with + */ + public synchronized void close(StatusCode statusCode) { + if (closed) { + return; + } + + LOGGER.warn("Closing connection: {}", statusCode.getDescription()); + socket.sendClose(statusCode.getCode(), statusCode.getDescriptionAsReason()); + + Timeout disconnectTimeout = Timeout.after(Delays.CLOSE_CONNECTION_TIMEOUT); + disconnectTimeout.getCompletionStage().thenAccept(aVoid -> forceClose(statusCode)); + disconnectTimeout.start(); + } + + /** + * Forcefully close the connection immediately. + * + * @param statusCode the status code to close the connection with + */ + public synchronized void forceClose(StatusCode statusCode) { + if (closed) { + return; + } + + LOGGER.warn("Force-closing connection: {}", statusCode.getDescription()); + socket.abort(); + + // Since neither onClose nor onError are called when the socket is aborted like this, we + // need to call cleanupAfterClosed here too. + cleanupAfterClosed(); + } + + /** + * Allow outside functions to switch the state machine state. Waits for a resting state and then + * switches to the specified state. + * + * @param state the state to switch to + * @return whether the state was switched successfully + * @throws InterruptedException if the thread got interrupted while waiting for the state machine + * to enter a resting state + */ + public boolean switchFromRestingState(RunnerState state) throws InterruptedException { + return stateMachine.switchFromRestingState(state); + } + + public Serializer getSerializer() { + return serializer; + } + + public Future getClosedFuture() { + return closedFuture; + } + + @Override + public void onOpen(WebSocket webSocket) { + webSocket.request(1); + + // Initial call by the websocket library, so we can use it to obtain our socket for socket-y + // purposes. + synchronized (this) { + socket = webSocket; + heartbeatHandler = new HeartbeatHandler(this); + } + } + + @Override + public synchronized CompletionStage onText( + WebSocket webSocket, CharSequence data, boolean last) { + + webSocket.request(1); + if (closed) { + return null; + } + + textPacketBuilder.append(data); + + if (last) { + String text = textPacketBuilder.toString(); + textPacketBuilder = new StringBuilder(); + stateMachine.changeCurrentState(state -> state.onText(text)); + } + + return null; + } + + @Override + public synchronized CompletionStage onBinary( + WebSocket webSocket, ByteBuffer data, boolean last) { + + webSocket.request(1); + if (closed) { + return null; + } + + stateMachine.changeCurrentState(state -> state.onBinary(data, last)); + + return null; + } + + @Override + public synchronized CompletionStage onClose( + WebSocket webSocket, int statusCode, String reason) { + + String statusCodeStr = + StatusCode.fromCode(statusCode).map(Enum::toString).orElse(Integer.toString(statusCode)); + LOGGER.debug("Connection closed normally, status code: {}", statusCodeStr); + cleanupAfterClosed(); + return null; + } + + @Override + public synchronized void onError(WebSocket webSocket, Throwable error) { + // For some reason, this function is not called after a socket.abort(). + LOGGER.warn("Connection closed abnormally"); + cleanupAfterClosed(); + } + + @Override + public CompletionStage onPong(WebSocket webSocket, ByteBuffer message) { + webSocket.request(1); + heartbeatHandler.onPong(); + return null; + } + + @Override + public void onTimeoutDetected() { + LOGGER.warn("Detected ping timeout, disconnecting"); + close(StatusCode.PING_TIMEOUT); + } + + @Override + public boolean sendPing() { + synchronized (this) { + if (closed) { + LOGGER.debug("Couldn't send ping because connection already closed"); + return false; + } + + socket.sendPing(ByteBuffer.wrap(Long.toString(System.currentTimeMillis()).getBytes())); + return true; + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Delays.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Delays.java index 93e9305cb..3ad9e7cf1 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Delays.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/Delays.java @@ -2,39 +2,31 @@ import java.time.Duration; -/** - * A class for keeping all kinds of runner-related delays and timeouts. - */ +/** A class for keeping all kinds of runner-related delays and timeouts. */ public final class Delays { - /** - * How long to wait for a reconnect when creating a connection fails. This delay prevents flooding - * the server on the other end with too many connection attempts. - */ - public static final Duration RECONNECT_AFTER_FAILED_CONNECTION = Duration.ofSeconds(10); - - /** - * How long to wait for a server response when trying to close a connection before force-closing - * it. - */ - public static final Duration CLOSE_CONNECTION_TIMEOUT = Duration.ofSeconds(10); - - /** - * How long to pause in-between rounds of asking the backends for new benchmarks. - */ - public static final Duration BACKEND_ROUNDTRIP = Duration.ofSeconds(10); - - /** - * How long to wait - after sending a command to the server - for a reply to that command. - */ - public static final Duration AWAIT_COMMAND_REPLY = Duration.ofSeconds(10); - - /** - * How long to wait until the sigkilling begins (in the Benchmarker). - */ - public static final Duration TIME_TO_KILL = Duration.ofSeconds(10); - - private Delays() { - throw new UnsupportedOperationException(); - } + /** + * How long to wait for a reconnect when creating a connection fails. This delay prevents flooding + * the server on the other end with too many connection attempts. + */ + public static final Duration RECONNECT_AFTER_FAILED_CONNECTION = Duration.ofSeconds(10); + + /** + * How long to wait for a server response when trying to close a connection before force-closing + * it. + */ + public static final Duration CLOSE_CONNECTION_TIMEOUT = Duration.ofSeconds(10); + + /** How long to pause in-between rounds of asking the backends for new benchmarks. */ + public static final Duration BACKEND_ROUNDTRIP = Duration.ofSeconds(10); + + /** How long to wait - after sending a command to the server - for a reply to that command. */ + public static final Duration AWAIT_COMMAND_REPLY = Duration.ofSeconds(10); + + /** How long to wait until the sigkilling begins (in the Benchmarker). */ + public static final Duration TIME_TO_KILL = Duration.ofSeconds(10); + + private Delays() { + throw new UnsupportedOperationException(); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerCliSpec.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerCliSpec.java index c4da102a1..34ec81941 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerCliSpec.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerCliSpec.java @@ -7,42 +7,33 @@ import net.jbock.Command; import net.jbock.Param; -/** - * The runner for VelCom that executes benchmarks. Made by Aaaaaaah! - */ +/** The runner for VelCom that executes benchmarks. Made by Aaaaaaah! */ @Command(value = "VelCom-Runner") public abstract class RunnerCliSpec { - /** - * The path to the config file. - */ - @Param(value = 1, mappedBy = ConfigFilePathMapper.class) - public abstract Path configFileLocation(); + /** The path to the config file. */ + @Param(value = 1, mappedBy = ConfigFilePathMapper.class) + public abstract Path configFileLocation(); - /** - * A mapper for the config file location. - */ - public static class ConfigFilePathMapper implements Function { + /** A mapper for the config file location. */ + public static class ConfigFilePathMapper implements Function { - @Override - public Path apply(String name) { - Path path = Paths.get(name); - if (Files.notExists(path)) { - throw new IllegalArgumentException( - "The given path (" + path.toAbsolutePath() + ") does not exist!" - ); - } - if (!Files.isRegularFile(path)) { - throw new IllegalArgumentException( - "The given path (" + path.toAbsolutePath() + ") is no regular file!" - ); - } - if (!Files.isReadable(path)) { - throw new IllegalArgumentException( - "The given path (" + path.toAbsolutePath() + ") is not readable by me!" - ); - } - return path.toAbsolutePath(); - } - } + @Override + public Path apply(String name) { + Path path = Paths.get(name); + if (Files.notExists(path)) { + throw new IllegalArgumentException( + "The given path (" + path.toAbsolutePath() + ") does not exist!"); + } + if (!Files.isRegularFile(path)) { + throw new IllegalArgumentException( + "The given path (" + path.toAbsolutePath() + ") is no regular file!"); + } + if (!Files.isReadable(path)) { + throw new IllegalArgumentException( + "The given path (" + path.toAbsolutePath() + ") is not readable by me!"); + } + return path.toAbsolutePath(); + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerConfig.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerConfig.java index a1024dd97..649324230 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerConfig.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerConfig.java @@ -14,56 +14,52 @@ @JsonIgnoreProperties("_comment") public class RunnerConfig { - private final String name; - private final List backends; + private final String name; + private final List backends; - @JsonCreator - public RunnerConfig( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) List backends - ) { - this.name = name; - this.backends = backends; - } + @JsonCreator + public RunnerConfig( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) List backends) { + this.name = name; + this.backends = backends; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public List getBackends() { - return backends; - } + public List getBackends() { + return backends; + } - /** - * Section describing a single backend and how the runner should connect to it. - */ - public static class BackendEntry { + /** Section describing a single backend and how the runner should connect to it. */ + public static class BackendEntry { - private final URI address; - private final String token; - private final Path directory; + private final URI address; + private final String token; + private final Path directory; - @JsonCreator - public BackendEntry( - @JsonProperty(required = true) URI address, - @JsonProperty(required = true) String token, - @JsonProperty(required = true) Path directory - ) { - this.address = address; - this.token = token; - this.directory = directory; - } + @JsonCreator + public BackendEntry( + @JsonProperty(required = true) URI address, + @JsonProperty(required = true) String token, + @JsonProperty(required = true) Path directory) { + this.address = address; + this.token = token; + this.directory = directory; + } - public URI getAddress() { - return address; - } + public URI getAddress() { + return address; + } - public String getToken() { - return token; - } + public String getToken() { + return token; + } - public Path getDirectory() { - return directory; - } - } + public Path getDirectory() { + return directory; + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerMain.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerMain.java index 4649e6a5f..9ba370245 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerMain.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/RunnerMain.java @@ -20,101 +20,109 @@ */ public class RunnerMain { - private static final Logger LOGGER = LoggerFactory.getLogger(RunnerMain.class); - - /** - * The runner's main class's main method. Starts the runner. - * - * @param args the command line arguments - */ - public static void main(String[] args) { - System.out.println("Welcome to VelCom!"); - System.out.printf("Version: %s (runner)%n", GitProperties.getVersion()); - System.out.printf("Build time: %s%n", GitProperties.getBuildTime()); - System.out.printf("Commit hash: %s%n", GitProperties.getHash()); - System.out.println(); - - try { - realMain(args); - } catch (InterruptedException | ExecutionException e) { - die(e, "Encountered irrecoverable exception"); - } - } - - private static void realMain(String[] args) throws ExecutionException, InterruptedException { - RunnerCliSpec cliSpec = new RunnerCliSpec_Parser().parseOrExit(args); - RunnerConfig config = loadConfig(cliSpec.configFileLocation()); - - AtomicReference globalStatus = new AtomicReference<>(Status.IDLE); - - List backends = config.getBackends().stream() - .map(entry -> new TeleBackend( - globalStatus, - entry.getAddress(), - config.getName(), - entry.getToken(), - entry.getDirectory()) - ) - .collect(Collectors.toList()); - - backends.forEach(backend -> new Thread(() -> { - try { - backend.run(); - } catch (InterruptedException e) { - die(e, "Thread running " + backend + " was interrupted. This should never happen."); - } - }).start()); - - LOGGER.debug("Waiting a bit before starting first backend round trip"); - Thread.sleep(Delays.BACKEND_ROUNDTRIP.toMillis()); - - //noinspection InfiniteLoopStatement - while (true) { - boolean didBenchmark = false; - - for (TeleBackend backend : backends) { - LOGGER.debug("Asking {} for a benchmark", backend); - didBenchmark |= backend.maybePerformBenchmark(); - } - - if (didBenchmark) { - LOGGER.debug("Did a benchmark in this iteration, there might be more"); - LOGGER.debug("Starting new backend round trip immediately"); - } else { - LOGGER.debug("No backend had any benchmark in this iteration"); - LOGGER.debug("Delaying new backend round trip for a bit"); - //noinspection BusyWait - Thread.sleep(Delays.BACKEND_ROUNDTRIP.toMillis()); - LOGGER.debug("Starting new backend round trip"); - } - } - } - - @Nonnull - private static RunnerConfig loadConfig(Path configFilePath) { - ObjectMapper objectMapper = new ObjectMapper() - .registerModule(new ParameterNamesModule()); - - try { - return objectMapper.readValue(configFilePath.toFile(), RunnerConfig.class); - } catch (IOException e) { - die(e, "Could not load config file at path " + configFilePath); - // never reached - //noinspection ConstantConditions - return null; - } - } - - /** - * Die with an error message and an exception. Always dies with exit code 1. - * - * @param e the exception to print - * @param message the message to print - */ - public static void die(Throwable e, String message) { - System.out.println(message); - System.out.println(e.toString()); - System.exit(1); - } - + private static final Logger LOGGER = LoggerFactory.getLogger(RunnerMain.class); + + /** + * The runner's main class's main method. Starts the runner. + * + * @param args the command line arguments + */ + public static void main(String[] args) { + System.out.println("Welcome to VelCom!"); + System.out.printf("Version: %s (runner)%n", GitProperties.getVersion()); + System.out.printf("Build time: %s%n", GitProperties.getBuildTime()); + System.out.printf("Commit hash: %s%n", GitProperties.getHash()); + System.out.println(); + + try { + realMain(args); + } catch (InterruptedException | ExecutionException e) { + die(e, "Encountered irrecoverable exception"); + } + } + + private static void realMain(String[] args) throws ExecutionException, InterruptedException { + RunnerCliSpec cliSpec = new RunnerCliSpec_Parser().parseOrExit(args); + RunnerConfig config = loadConfig(cliSpec.configFileLocation()); + + AtomicReference globalStatus = new AtomicReference<>(Status.IDLE); + + List backends = + config.getBackends().stream() + .map( + entry -> + new TeleBackend( + globalStatus, + entry.getAddress(), + config.getName(), + entry.getToken(), + entry.getDirectory())) + .collect(Collectors.toList()); + + backends.forEach( + backend -> + new Thread( + () -> { + try { + backend.run(); + } catch (InterruptedException e) { + die( + e, + "Thread running " + + backend + + " was interrupted. This should never happen."); + } + }) + .start()); + + LOGGER.debug("Waiting a bit before starting first backend round trip"); + Thread.sleep(Delays.BACKEND_ROUNDTRIP.toMillis()); + + //noinspection InfiniteLoopStatement + while (true) { + boolean didBenchmark = false; + + for (TeleBackend backend : backends) { + LOGGER.debug("Asking {} for a benchmark", backend); + didBenchmark |= backend.maybePerformBenchmark(); + } + + if (didBenchmark) { + LOGGER.debug("Did a benchmark in this iteration, there might be more"); + LOGGER.debug("Starting new backend round trip immediately"); + } else { + LOGGER.debug("No backend had any benchmark in this iteration"); + LOGGER.debug("Delaying new backend round trip for a bit"); + //noinspection BusyWait + Thread.sleep(Delays.BACKEND_ROUNDTRIP.toMillis()); + LOGGER.debug("Starting new backend round trip"); + } + } + } + + @Nonnull + private static RunnerConfig loadConfig(Path configFilePath) { + ObjectMapper objectMapper = new ObjectMapper().registerModule(new ParameterNamesModule()); + + try { + return objectMapper.readValue(configFilePath.toFile(), RunnerConfig.class); + } catch (IOException e) { + die(e, "Could not load config file at path " + configFilePath); + // never reached + //noinspection ConstantConditions + return null; + } + } + + /** + * Die with an error message and an exception. Always dies with exit code 1. + * + * @param e the exception to print + * @param message the message to print + */ + public static void die(Throwable e, String message) { + System.out.println(message); + System.out.println(e.toString()); + System.exit(1); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/TeleBackend.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/TeleBackend.java index a667396ed..8f6b8832a 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/TeleBackend.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/TeleBackend.java @@ -29,320 +29,318 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * This class contains all the data and objects which are valid during the life of a backend. - */ +/** This class contains all the data and objects which are valid during the life of a backend. */ public class TeleBackend { - private static final Logger LOGGER = LoggerFactory.getLogger(TeleBackend.class); - - private final AtomicReference globalStatus; - - private final URI address; - private final String name; - private final String token; - - private final BenchRepoDir benchRepoDir; - private final TaskRepoDir taskRepoDir; - - // Protects benchmarker - private final Object benchmarkerLock; - @Nullable - private Benchmarker benchmarker; - - public TeleBackend(AtomicReference globalStatus, URI address, String name, String token, - Path path) { - - this.globalStatus = globalStatus; - - this.address = address; - this.name = name; - this.token = token; - - // RunnerMain.die will stop the runner but java doesn't know that, so I need to do this - // little dance here... - BenchRepoDir tmpBenchRepoDir = null; - try { - tmpBenchRepoDir = new BenchRepoDir(path.resolve("bench_repo")); - } catch (IOException e) { - // Dying here is still okay since the main thread starts the TeleBackend threads only - // when all TeleBackends have been initialized successfully. That's why we don't need - // to close any open connections or stop running benchmarks here. - RunnerMain.die(e, "Could not load hash file of bench repo"); - } - benchRepoDir = tmpBenchRepoDir; - taskRepoDir = new TaskRepoDir(path.resolve("task_repo")); - - benchmarkerLock = new Object(); - benchmarker = null; - } - - @Nullable - private volatile Connection connection; - - /** - * Start and run an infinite loop that connects to the backend and communicates with it. - * - * @throws InterruptedException if the thread is interrupted - */ - public void run() throws InterruptedException { - // Shared HttpClient to prevent FDs leaking based on GC - // - // A HttpClient initially creates/opens a few file descriptors. It only frees those file - // descriptors once no more references to the HttpClient exist. This usually requires garbage - // collection to take place. If a new HttpClient is created for each connection attempt but - // garbage collection only happens infrequently, file descriptors are "leaked". This can easily - // be prevented by using a single HttpClient instance for all connection attempts. - HttpClient httpClient = HttpClient.newHttpClient(); - - //noinspection InfiniteLoopStatement - while (true) { - try { - LOGGER.info("{} - Connecting", address); - Connection conn = new Connection(this, httpClient, address, name, token); - LOGGER.info("{} - Connected", address); - connection = conn; - conn.getClosedFuture().get(); - LOGGER.info("{} - Disconnected, reconnecting immediately", address); - } catch (ExecutionException | InterruptedException e) { - LOGGER.warn("{} - Failed to connect, retrying in {}", address, - Delays.RECONNECT_AFTER_FAILED_CONNECTION); - //noinspection BusyWait - Thread.sleep(Delays.RECONNECT_AFTER_FAILED_CONNECTION.toMillis()); - } - } - } - - /** - * Performs a benchmark if possible, asking the backend in the process. - * - * This function must always be called from the same thread! - * - * @return true if a benchmark was performed, false otherwise - * @throws InterruptedException in an irrecoverable situation (runner must die) - * @throws ExecutionException in an irrecoverable situation (runner must die) - */ - public boolean maybePerformBenchmark() throws InterruptedException, ExecutionException { - // This function is only here to ensure the globalStatus is always set correctly. The meat - // of the beast is in maybePerformBenchmarkHelper, of course. - globalStatus.set(Status.RUN); - try { - return maybePerformBenchmarkHelper(); - } finally { - globalStatus.set(Status.IDLE); - } - } - - private boolean maybePerformBenchmarkHelper() throws InterruptedException, ExecutionException { - Optional benchResult = getBenchResult(); - if (benchResult.isPresent()) { - // The runner protocol forbids us from asking the backend for a new task while we still - // have a result, so we just ignore the request. - return false; - } - // Since this function is the only place where the benchmarker can be set to a non-null - // value, and this function only returns after the benchmarker has a result (or is still - // null in the first place), we now know that there is no benchmarker. - - Connection conn = connection; - if (conn == null) { - // We obviously can't ask the backend for a task if we aren't even connected to it. - return false; - } - - try { - clearTmpFiles(); - } catch (IOException e) { - LOGGER.warn("{} - Could not clear temporary files", address, e); - return false; - } - - // This future will complete as soon as all tar files have been downloaded. - CompletableFuture replyFuture = sendRequestRun(conn); - - RequestRunReply reply; - try { - reply = replyFuture.get(); - } catch (ExecutionException | CancellationException e) { - LOGGER.debug( - "{} - Backend has no new files or something went wrong while trying to download them", - address - ); - return false; - } - - try { - unpackTmpFiles(reply.hasBench(), reply.hasRun()); - if (reply.getBenchHash().isPresent()) { - benchRepoDir.setHash(reply.getBenchHash().get()); - } - } catch (IOException e) { - LOGGER.warn("{} - Could not unpack tar files", address, e); - return false; - } - try { - clearTmpFiles(); - } catch (IOException e) { - LOGGER.warn("{} - Could not clear temporary files", address, e); - return false; - } - - if (reply.getRunId().isPresent()) { - UUID taskId = reply.getRunId().get(); - LOGGER.info("{} - Starting benchmark for task {}", address, taskId); - Boolean success = startBenchmark(taskId).get(); - if (success) { - LOGGER.info("{} - Benchmark for task {} completed successfully", address, taskId); - } else { - LOGGER.info("{} - Benchmark for task {} completed unsuccessfully", address, taskId); - } - return true; - } - - return false; - } - - private void clearTmpFiles() throws IOException { - FileHelper.deleteDirectoryOrFile(benchRepoDir.getTmpFilePath()); - FileHelper.deleteDirectoryOrFile(taskRepoDir.getTmpFilePath()); - } - - private void unpackTmpFiles(boolean benchRepo, boolean taskRepo) throws IOException { - if (benchRepo) { - FileHelper.deleteDirectoryOrFile(benchRepoDir.getDirPath()); - TarHelper.untar(benchRepoDir.getTmpFilePath(), benchRepoDir.getDirPath()); - } - - if (taskRepo) { - FileHelper.deleteDirectoryOrFile(taskRepoDir.getDirPath()); - TarHelper.untar(taskRepoDir.getTmpFilePath(), taskRepoDir.getDirPath()); - } - } - - private CompletableFuture sendRequestRun(Connection conn) - throws InterruptedException { - - CompletableFuture replyFuture = new CompletableFuture<>(); - RunnerState newState = new AwaitingRequestRunReply(this, conn, replyFuture); - - boolean switchSuccessful = conn.switchFromRestingState(newState); - if (switchSuccessful) { - conn.sendPacket(new RequestRun().asPacket(conn.getSerializer())); - } else { - replyFuture.cancel(true); - } - - return replyFuture; - } - - private CompletableFuture startBenchmark(UUID runId) { - // The benchmarker is guaranteed to be null. For more detail, see the comment at the - // top of maybePerformBenchmark. - - CompletableFuture benchmarkFinished = new CompletableFuture<>(); - - Benchmarker newBenchmarker = new Benchmarker( - benchmarkFinished, - runId, - taskRepoDir.getDirPath(), - benchRepoDir.getHash().orElse(null), - benchRepoDir.getDirPath(), - Instant.now(), - name, - LinuxSystemInfo.getCurrent() - ); - - synchronized (benchmarkerLock) { - benchmarker = newBenchmarker; - } - - return benchmarkFinished; - } - - public NamedRows getInfo() { - LinuxSystemInfo systemInfo = LinuxSystemInfo.getCurrent(); - NamedRows info = new NamedRows(); - - info.add("System", - System.getProperty("os.name") - + " " + System.getProperty("os.arch") - + " " + System.getProperty("os.version")); - - info.add("CPU", systemInfo.getCpuInfo().format()); - info.add("Memory", systemInfo.getMemoryInfo().format()); - - return info; - } - - public Status getStatus() { - return globalStatus.get(); - } - - public Optional getBenchHash() { - return benchRepoDir.getHash(); - } - - public Optional getBenchResult() { - return getBenchmarker().flatMap(Benchmarker::getResult); - } - - /** - * Attempt to clear the benchmark result. - * - * @return true if successful, false otherwise - */ - public boolean clearBenchResult() { - synchronized (benchmarkerLock) { - if (benchmarker != null && benchmarker.getResult().isPresent()) { - benchmarker = null; - return true; - } else { - return false; - } - } - } - - /** - * Try to abort the current run. Does nothing if no run is currently running. - */ - public void abortCurrentRun() { - getBenchmarker().ifPresent(benchmarker -> { - globalStatus.set(Status.ABORT); - benchmarker.abort(); - LOGGER.warn("The current run was aborted"); - }); - } - - public Optional getCurrentRunId() { - return getBenchmarker().map(Benchmarker::getTaskId); - } - - public Optional getLastOutputLines() { - return getBenchmarker().map(Benchmarker::getLastOutputLines); - } - - private Optional getBenchmarker() { - synchronized (benchmarkerLock) { - return Optional.ofNullable(benchmarker); - } - } - - public Path getBenchRepoTmpPath() { - return benchRepoDir.getTmpFilePath(); - } - - public Path getTaskRepoTmpPath() { - return taskRepoDir.getTmpFilePath(); - } - - public URI getAddress() { - return address; - } - - @Override - public String toString() { - return "TeleBackend{" + - "address=" + address + - '}'; - } + private static final Logger LOGGER = LoggerFactory.getLogger(TeleBackend.class); + + private final AtomicReference globalStatus; + + private final URI address; + private final String name; + private final String token; + + private final BenchRepoDir benchRepoDir; + private final TaskRepoDir taskRepoDir; + + // Protects benchmarker + private final Object benchmarkerLock; + @Nullable private Benchmarker benchmarker; + + public TeleBackend( + AtomicReference globalStatus, URI address, String name, String token, Path path) { + + this.globalStatus = globalStatus; + + this.address = address; + this.name = name; + this.token = token; + + // RunnerMain.die will stop the runner but java doesn't know that, so I need to do this + // little dance here... + BenchRepoDir tmpBenchRepoDir = null; + try { + tmpBenchRepoDir = new BenchRepoDir(path.resolve("bench_repo")); + } catch (IOException e) { + // Dying here is still okay since the main thread starts the TeleBackend threads only + // when all TeleBackends have been initialized successfully. That's why we don't need + // to close any open connections or stop running benchmarks here. + RunnerMain.die(e, "Could not load hash file of bench repo"); + } + benchRepoDir = tmpBenchRepoDir; + taskRepoDir = new TaskRepoDir(path.resolve("task_repo")); + + benchmarkerLock = new Object(); + benchmarker = null; + } + + @Nullable private volatile Connection connection; + + /** + * Start and run an infinite loop that connects to the backend and communicates with it. + * + * @throws InterruptedException if the thread is interrupted + */ + public void run() throws InterruptedException { + // Shared HttpClient to prevent FDs leaking based on GC + // + // A HttpClient initially creates/opens a few file descriptors. It only frees those file + // descriptors once no more references to the HttpClient exist. This usually requires garbage + // collection to take place. If a new HttpClient is created for each connection attempt but + // garbage collection only happens infrequently, file descriptors are "leaked". This can easily + // be prevented by using a single HttpClient instance for all connection attempts. + HttpClient httpClient = HttpClient.newHttpClient(); + + //noinspection InfiniteLoopStatement + while (true) { + try { + LOGGER.info("{} - Connecting", address); + Connection conn = new Connection(this, httpClient, address, name, token); + LOGGER.info("{} - Connected", address); + connection = conn; + conn.getClosedFuture().get(); + LOGGER.info("{} - Disconnected, reconnecting immediately", address); + } catch (ExecutionException | InterruptedException e) { + LOGGER.warn( + "{} - Failed to connect, retrying in {}", + address, + Delays.RECONNECT_AFTER_FAILED_CONNECTION); + //noinspection BusyWait + Thread.sleep(Delays.RECONNECT_AFTER_FAILED_CONNECTION.toMillis()); + } + } + } + + /** + * Performs a benchmark if possible, asking the backend in the process. + * + *

This function must always be called from the same thread! + * + * @return true if a benchmark was performed, false otherwise + * @throws InterruptedException in an irrecoverable situation (runner must die) + * @throws ExecutionException in an irrecoverable situation (runner must die) + */ + public boolean maybePerformBenchmark() throws InterruptedException, ExecutionException { + // This function is only here to ensure the globalStatus is always set correctly. The meat + // of the beast is in maybePerformBenchmarkHelper, of course. + globalStatus.set(Status.RUN); + try { + return maybePerformBenchmarkHelper(); + } finally { + globalStatus.set(Status.IDLE); + } + } + + private boolean maybePerformBenchmarkHelper() throws InterruptedException, ExecutionException { + Optional benchResult = getBenchResult(); + if (benchResult.isPresent()) { + // The runner protocol forbids us from asking the backend for a new task while we still + // have a result, so we just ignore the request. + return false; + } + // Since this function is the only place where the benchmarker can be set to a non-null + // value, and this function only returns after the benchmarker has a result (or is still + // null in the first place), we now know that there is no benchmarker. + + Connection conn = connection; + if (conn == null) { + // We obviously can't ask the backend for a task if we aren't even connected to it. + return false; + } + + try { + clearTmpFiles(); + } catch (IOException e) { + LOGGER.warn("{} - Could not clear temporary files", address, e); + return false; + } + + // This future will complete as soon as all tar files have been downloaded. + CompletableFuture replyFuture = sendRequestRun(conn); + + RequestRunReply reply; + try { + reply = replyFuture.get(); + } catch (ExecutionException | CancellationException e) { + LOGGER.debug( + "{} - Backend has no new files or something went wrong while trying to download them", + address); + return false; + } + + try { + unpackTmpFiles(reply.hasBench(), reply.hasRun()); + if (reply.getBenchHash().isPresent()) { + benchRepoDir.setHash(reply.getBenchHash().get()); + } + } catch (IOException e) { + LOGGER.warn("{} - Could not unpack tar files", address, e); + return false; + } + try { + clearTmpFiles(); + } catch (IOException e) { + LOGGER.warn("{} - Could not clear temporary files", address, e); + return false; + } + + if (reply.getRunId().isPresent()) { + UUID taskId = reply.getRunId().get(); + LOGGER.info("{} - Starting benchmark for task {}", address, taskId); + Boolean success = startBenchmark(taskId).get(); + if (success) { + LOGGER.info("{} - Benchmark for task {} completed successfully", address, taskId); + } else { + LOGGER.info("{} - Benchmark for task {} completed unsuccessfully", address, taskId); + } + return true; + } + + return false; + } + + private void clearTmpFiles() throws IOException { + FileHelper.deleteDirectoryOrFile(benchRepoDir.getTmpFilePath()); + FileHelper.deleteDirectoryOrFile(taskRepoDir.getTmpFilePath()); + } + + private void unpackTmpFiles(boolean benchRepo, boolean taskRepo) throws IOException { + if (benchRepo) { + FileHelper.deleteDirectoryOrFile(benchRepoDir.getDirPath()); + TarHelper.untar(benchRepoDir.getTmpFilePath(), benchRepoDir.getDirPath()); + } + + if (taskRepo) { + FileHelper.deleteDirectoryOrFile(taskRepoDir.getDirPath()); + TarHelper.untar(taskRepoDir.getTmpFilePath(), taskRepoDir.getDirPath()); + } + } + + private CompletableFuture sendRequestRun(Connection conn) + throws InterruptedException { + + CompletableFuture replyFuture = new CompletableFuture<>(); + RunnerState newState = new AwaitingRequestRunReply(this, conn, replyFuture); + + boolean switchSuccessful = conn.switchFromRestingState(newState); + if (switchSuccessful) { + conn.sendPacket(new RequestRun().asPacket(conn.getSerializer())); + } else { + replyFuture.cancel(true); + } + + return replyFuture; + } + + private CompletableFuture startBenchmark(UUID runId) { + // The benchmarker is guaranteed to be null. For more detail, see the comment at the + // top of maybePerformBenchmark. + + CompletableFuture benchmarkFinished = new CompletableFuture<>(); + + Benchmarker newBenchmarker = + new Benchmarker( + benchmarkFinished, + runId, + taskRepoDir.getDirPath(), + benchRepoDir.getHash().orElse(null), + benchRepoDir.getDirPath(), + Instant.now(), + name, + LinuxSystemInfo.getCurrent()); + + synchronized (benchmarkerLock) { + benchmarker = newBenchmarker; + } + + return benchmarkFinished; + } + + public NamedRows getInfo() { + LinuxSystemInfo systemInfo = LinuxSystemInfo.getCurrent(); + NamedRows info = new NamedRows(); + + info.add( + "System", + System.getProperty("os.name") + + " " + + System.getProperty("os.arch") + + " " + + System.getProperty("os.version")); + + info.add("CPU", systemInfo.getCpuInfo().format()); + info.add("Memory", systemInfo.getMemoryInfo().format()); + + return info; + } + + public Status getStatus() { + return globalStatus.get(); + } + + public Optional getBenchHash() { + return benchRepoDir.getHash(); + } + + public Optional getBenchResult() { + return getBenchmarker().flatMap(Benchmarker::getResult); + } + + /** + * Attempt to clear the benchmark result. + * + * @return true if successful, false otherwise + */ + public boolean clearBenchResult() { + synchronized (benchmarkerLock) { + if (benchmarker != null && benchmarker.getResult().isPresent()) { + benchmarker = null; + return true; + } else { + return false; + } + } + } + + /** Try to abort the current run. Does nothing if no run is currently running. */ + public void abortCurrentRun() { + getBenchmarker() + .ifPresent( + benchmarker -> { + globalStatus.set(Status.ABORT); + benchmarker.abort(); + LOGGER.warn("The current run was aborted"); + }); + } + + public Optional getCurrentRunId() { + return getBenchmarker().map(Benchmarker::getTaskId); + } + + public Optional getLastOutputLines() { + return getBenchmarker().map(Benchmarker::getLastOutputLines); + } + + private Optional getBenchmarker() { + synchronized (benchmarkerLock) { + return Optional.ofNullable(benchmarker); + } + } + + public Path getBenchRepoTmpPath() { + return benchRepoDir.getTmpFilePath(); + } + + public Path getTaskRepoTmpPath() { + return taskRepoDir.getTmpFilePath(); + } + + public URI getAddress() { + return address; + } + + @Override + public String toString() { + return "TeleBackend{" + "address=" + address + '}'; + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/BenchResult.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/BenchResult.java index bd4f1d9bb..064111d2b 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/BenchResult.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/BenchResult.java @@ -5,72 +5,70 @@ import java.time.Instant; import java.util.UUID; -/** - * The result of trying to do a run on the runner. - */ +/** The result of trying to do a run on the runner. */ public class BenchResult { - private final UUID runId; - private final Either result; - private final Instant startTime; - private final Instant stopTime; + private final UUID runId; + private final Either result; + private final Instant startTime; + private final Instant stopTime; - private BenchResult(UUID runId, Either result, Instant startTime, - Instant stopTime) { + private BenchResult( + UUID runId, Either result, Instant startTime, Instant stopTime) { - this.runId = runId; - this.result = result; - this.startTime = startTime; - this.stopTime = stopTime; - } + this.runId = runId; + this.result = result; + this.startTime = startTime; + this.stopTime = stopTime; + } - /** - * Create a new successful benchmark result. A bench result is successful if the runner executed - * the bench script correctly and the bench script printed valid output (even a global error). - * - * @param runId the run's id - * @param result the parsed bench script output - * @param startTime when the run started - * @param stopTime when the run finished - * @return the newly created bench result - */ - public static BenchResult successful(UUID runId, Result result, Instant startTime, - Instant stopTime) { + /** + * Create a new successful benchmark result. A bench result is successful if the runner executed + * the bench script correctly and the bench script printed valid output (even a global error). + * + * @param runId the run's id + * @param result the parsed bench script output + * @param startTime when the run started + * @param stopTime when the run finished + * @return the newly created bench result + */ + public static BenchResult successful( + UUID runId, Result result, Instant startTime, Instant stopTime) { - return new BenchResult(runId, Either.ofRight(result), startTime, stopTime); - } + return new BenchResult(runId, Either.ofRight(result), startTime, stopTime); + } - /** - * Create a new failed benchmark result. A bench result is failed if the runner could not execute - * the bench script correctly or the bench script printed invalid output. - * - * @param runId the run's id - * @param error a string describing the failure - * @param startTime when the run was started - * @param stopTime when the run was finished - * @return the newly created bench result - */ - public static BenchResult failed(UUID runId, String error, Instant startTime, Instant stopTime) { - return new BenchResult(runId, Either.ofLeft(error), startTime, stopTime); - } + /** + * Create a new failed benchmark result. A bench result is failed if the runner could not execute + * the bench script correctly or the bench script printed invalid output. + * + * @param runId the run's id + * @param error a string describing the failure + * @param startTime when the run was started + * @param stopTime when the run was finished + * @return the newly created bench result + */ + public static BenchResult failed(UUID runId, String error, Instant startTime, Instant stopTime) { + return new BenchResult(runId, Either.ofLeft(error), startTime, stopTime); + } - public UUID getRunId() { - return runId; - } + public UUID getRunId() { + return runId; + } - public boolean isSuccess() { - return result.isRight(); - } + public boolean isSuccess() { + return result.isRight(); + } - public Either getResult() { - return result; - } + public Either getResult() { + return result; + } - public Instant getStartTime() { - return startTime; - } + public Instant getStartTime() { + return startTime; + } - public Instant getStopTime() { - return stopTime; - } + public Instant getStopTime() { + return stopTime; + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/Benchmarker.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/Benchmarker.java index c5302397d..a754d23fe 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/Benchmarker.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/Benchmarker.java @@ -31,289 +31,285 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -/** - * Executes the benchmark script and parses the result. - */ +/** Executes the benchmark script and parses the result. */ public class Benchmarker { - private final AtomicReference result; // nullable inside the reference - private final AtomicReference> outputFetcher; // nullable inside the reference - - private final CompletableFuture finishFuture; - - private final UUID taskId; - private final Path taskRepoPath; - @Nullable - private final String benchRepoHash; - private final Path benchRepoPath; - - private final Instant startTime; - private final String runnerName; - private final LinuxSystemInfo systemInfo; - - private volatile boolean aborted; - private final Thread worker; - - /** - * Creates a new Benchmarker and starts the benchmark. - * - * @param finishFuture the future the benchmarker will complete after the benchmark is done - * (aborted, failed or completed successfully) and after a corresponding result has been set - * @param taskId the UUID of the task/run this benchmarker will perform - * @param taskRepoPath the path to the task repo directory - * @param benchRepoHash the current hash of the bench repo, or null if the backend has not yet - * sent us any bench repo. - * @param benchRepoPath the path to the bench repo directory - * @param startTime the time when the benchmark process started. When exactly this process starts - * is not up to the benchmarker. - * @param runnerName the name of this runner - * @param systemInfo the system information of this runner - */ - public Benchmarker(CompletableFuture finishFuture, UUID taskId, Path taskRepoPath, - @Nullable String benchRepoHash, Path benchRepoPath, Instant startTime, String runnerName, - LinuxSystemInfo systemInfo) { - - this.result = new AtomicReference<>(); - this.outputFetcher = new AtomicReference<>(); - - this.finishFuture = finishFuture; - - this.taskId = taskId; - this.taskRepoPath = taskRepoPath; - this.benchRepoHash = benchRepoHash; - this.benchRepoPath = benchRepoPath; - - this.startTime = startTime; - this.runnerName = runnerName; - this.systemInfo = systemInfo; - - aborted = false; - worker = new Thread(this::runBenchmark); - worker.start(); - } - - /** - * @return the id of the task this benchmarker is benchmarking - */ - public UUID getTaskId() { - return taskId; - } - - /** - * @return the last few lines of the output (stderr) or an empty String if none yet - */ - public LinesWithOffset getLastOutputLines() { - Supplier supplier = outputFetcher.get(); - if (supplier == null) { - return new LinesWithOffset(0, List.of()); - } - return supplier.get(); - } - - /** - * @return the benchmark result. - */ - public Optional getResult() { - return Optional.ofNullable(result.get()); - } - - private NamedRows getBasicInfo() { - NamedRows rows = new NamedRows(); - rows.add("Runner name", runnerName); - rows.add("Hash", GitProperties.getHash()); - - rows.add( - "System", - System.getProperty("os.name") - + " " + System.getProperty("os.arch") - + " " + System.getProperty("os.version") - ); - - rows.add("CPU", systemInfo.getCpuInfo().format()); - rows.add("Memory", systemInfo.getMemoryInfo().format()); - - rows.add( - "Java version", - System.getProperty("java.version") - + " by " + System.getProperty("java.vendor") - ); - - rows.add("User", System.getProperty("user.name")); - rows.add("Bench repo hash", Objects.requireNonNullElse(benchRepoHash, "none")); - - return rows; - } - - /** - * Aborts the benchmark if it is running. - */ - public void abort() { - aborted = true; - worker.interrupt(); - } - - private void runBenchmark() { - NamedRows generalInfo = getBasicInfo(); - NamedSections infoSections = new NamedSections(); - infoSections.addSection("General", generalInfo); - - Path benchScriptPath = benchRepoPath.resolve("bench"); - - if (!Files.isReadable(benchScriptPath)) { - infoSections.addSection("Setup error", "`bench` script not found or not readable"); - setResult(failedBenchResult(infoSections)); - return; - } - if (!Files.isExecutable(benchScriptPath)) { - infoSections.addSection("Setup error", "`bench` script is not executable"); - setResult(failedBenchResult(infoSections)); - return; - } - - StreamsProcessOutput work = startBenchExecution(generalInfo, benchScriptPath); - - outputFetcher.set(() -> { - String stdErr = work.getCurrentStdErr(); - List lines = stdErr.lines().collect(Collectors.toList()); - - int maxLinesToReturn = 100; - List sublist = lines - .subList(Math.max(lines.size() - maxLinesToReturn, 0), lines.size()); - - int indexFirstLine = lines.size() - sublist.size(); - - return new LinesWithOffset(indexFirstLine, sublist); - }); - - try { - // Ensure we do not wait if the benchmark was cancelled before this thread was ready - if (aborted) { - work.cancel(true); - } - ProgramResult programResult = work.get(); - - addProgramOutput(generalInfo, infoSections, programResult); - - setResult(interpretResult(infoSections, programResult)); - } catch (ExecutionException e) { - setResult(interpretExecutionException(infoSections, e)); - } catch (InterruptedException e) { - work.cancel(true); - infoSections.addSection("Failed", "The benchmark thread was interrupted"); - setResult(failedBenchResult(infoSections)); - } catch (CancellationException e) { - infoSections.addSection("Failed", "The run was aborted"); - setResult(failedBenchResult(infoSections)); - } - } - - /** - * Sets the result if it hasn't already be set. - * - * @param result the result - */ - private void setResult(@Nonnull BenchResult result) { - this.result.compareAndSet(null, result); - finishFuture.complete(result.isSuccess()); - } - - private StreamsProcessOutput startBenchExecution(NamedRows generalInfo, - Path benchScriptPath) { - Instant startTime = Instant.now(); - - String[] calledCommand = { - benchScriptPath.toAbsolutePath().toString(), - taskRepoPath.toAbsolutePath().toString() - }; - generalInfo.addEscapedArray("Executed command", calledCommand); - generalInfo.add("Start time", startTime.toString()); - - return new ProgramExecutor(Delays.TIME_TO_KILL.toMillis()).execute(calledCommand); - } - - private void addProgramOutput(NamedRows generalInfo, NamedSections infoSections, - ProgramResult programResult) { - - generalInfo.add("Stop time", Instant.now().toString()); - generalInfo.add("Execution time", programResult.getRuntime().toString()); - generalInfo.add("Exit code", programResult.getExitCode() + ""); - - infoSections.addSection( - "Stdout", - programResult.getStdOut().isEmpty() ? "" : programResult.getStdOut() - ); - infoSections.addSection( - "Stderr", - programResult.getStdErr().isEmpty() ? "" : programResult.getStdErr() - ); - } - - private BenchResult interpretResult(NamedSections infoSections, ProgramResult programResult) { - if (programResult.getExitCode() == 0) { - return interpretZeroExitCode(infoSections, programResult); - } - return interpretFailingExitCode(infoSections, programResult); - } - - private BenchResult interpretZeroExitCode(NamedSections infoSections, - ProgramResult programResult) { - - Either> bareResult; - - try { - bareResult = new BenchmarkScriptOutputParser().parse(programResult.getStdOut()); - } catch (OutputParseException e) { - infoSections.addSection("Invalid output", e.getMessage()); - infoSections.addSection( - "Reason", - "The benchmark script returned invalid output!" - ); - return failedBenchResult(infoSections); - } - - return successfulBenchResult(new Result( - bareResult.getRight().orElse(null), - bareResult.getLeft().orElse(null) - )); - } - - private BenchResult interpretFailingExitCode(NamedSections infoSections, - ProgramResult programResult) { - - infoSections.addSection( - "Reason", - "The benchmark script terminated with a non-zero exit code" - + " (" + programResult.getExitCode() + ")!" - ); - - LinuxSignal.forExitCode(programResult.getExitCode()) - .ifPresent(signal -> infoSections.addSection( - "Exit code interpretation", - "The exit code looks like the linux signal " + signal.name() + ".\n" - + "It's signal number is " + signal.getNumber() + " and it is caused by '" - + signal.getExplanation() + "'." - )); - - return failedBenchResult(infoSections); - } - - private BenchResult interpretExecutionException(NamedSections infoSections, - ExecutionException e) { - infoSections.addSection("Stacktrace", ExceptionHelper.getStackTrace(e)); - infoSections.addSection("End time", Instant.now().toString()); - infoSections.addSection( - "Reason", - "Maybe an internal runner error. Rebenchmarking might solve the problem!" - ); - return failedBenchResult(infoSections); - } - - private BenchResult successfulBenchResult(Result result) { - return BenchResult.successful(taskId, result, startTime, Instant.now()); - } - - private BenchResult failedBenchResult(NamedSections infoSections) { - return BenchResult.failed(taskId, infoSections.format(), startTime, Instant.now()); - } - + private final AtomicReference result; // nullable inside the reference + private final AtomicReference> + outputFetcher; // nullable inside the reference + + private final CompletableFuture finishFuture; + + private final UUID taskId; + private final Path taskRepoPath; + @Nullable private final String benchRepoHash; + private final Path benchRepoPath; + + private final Instant startTime; + private final String runnerName; + private final LinuxSystemInfo systemInfo; + + private volatile boolean aborted; + private final Thread worker; + + /** + * Creates a new Benchmarker and starts the benchmark. + * + * @param finishFuture the future the benchmarker will complete after the benchmark is done + * (aborted, failed or completed successfully) and after a corresponding result has been set + * @param taskId the UUID of the task/run this benchmarker will perform + * @param taskRepoPath the path to the task repo directory + * @param benchRepoHash the current hash of the bench repo, or null if the backend has not yet + * sent us any bench repo. + * @param benchRepoPath the path to the bench repo directory + * @param startTime the time when the benchmark process started. When exactly this process starts + * is not up to the benchmarker. + * @param runnerName the name of this runner + * @param systemInfo the system information of this runner + */ + public Benchmarker( + CompletableFuture finishFuture, + UUID taskId, + Path taskRepoPath, + @Nullable String benchRepoHash, + Path benchRepoPath, + Instant startTime, + String runnerName, + LinuxSystemInfo systemInfo) { + + this.result = new AtomicReference<>(); + this.outputFetcher = new AtomicReference<>(); + + this.finishFuture = finishFuture; + + this.taskId = taskId; + this.taskRepoPath = taskRepoPath; + this.benchRepoHash = benchRepoHash; + this.benchRepoPath = benchRepoPath; + + this.startTime = startTime; + this.runnerName = runnerName; + this.systemInfo = systemInfo; + + aborted = false; + worker = new Thread(this::runBenchmark); + worker.start(); + } + + /** + * @return the id of the task this benchmarker is benchmarking + */ + public UUID getTaskId() { + return taskId; + } + + /** + * @return the last few lines of the output (stderr) or an empty String if none yet + */ + public LinesWithOffset getLastOutputLines() { + Supplier supplier = outputFetcher.get(); + if (supplier == null) { + return new LinesWithOffset(0, List.of()); + } + return supplier.get(); + } + + /** + * @return the benchmark result. + */ + public Optional getResult() { + return Optional.ofNullable(result.get()); + } + + private NamedRows getBasicInfo() { + NamedRows rows = new NamedRows(); + rows.add("Runner name", runnerName); + rows.add("Hash", GitProperties.getHash()); + + rows.add( + "System", + System.getProperty("os.name") + + " " + + System.getProperty("os.arch") + + " " + + System.getProperty("os.version")); + + rows.add("CPU", systemInfo.getCpuInfo().format()); + rows.add("Memory", systemInfo.getMemoryInfo().format()); + + rows.add( + "Java version", + System.getProperty("java.version") + " by " + System.getProperty("java.vendor")); + + rows.add("User", System.getProperty("user.name")); + rows.add("Bench repo hash", Objects.requireNonNullElse(benchRepoHash, "none")); + + return rows; + } + + /** Aborts the benchmark if it is running. */ + public void abort() { + aborted = true; + worker.interrupt(); + } + + private void runBenchmark() { + NamedRows generalInfo = getBasicInfo(); + NamedSections infoSections = new NamedSections(); + infoSections.addSection("General", generalInfo); + + Path benchScriptPath = benchRepoPath.resolve("bench"); + + if (!Files.isReadable(benchScriptPath)) { + infoSections.addSection("Setup error", "`bench` script not found or not readable"); + setResult(failedBenchResult(infoSections)); + return; + } + if (!Files.isExecutable(benchScriptPath)) { + infoSections.addSection("Setup error", "`bench` script is not executable"); + setResult(failedBenchResult(infoSections)); + return; + } + + StreamsProcessOutput work = startBenchExecution(generalInfo, benchScriptPath); + + outputFetcher.set( + () -> { + String stdErr = work.getCurrentStdErr(); + List lines = stdErr.lines().collect(Collectors.toList()); + + int maxLinesToReturn = 100; + List sublist = + lines.subList(Math.max(lines.size() - maxLinesToReturn, 0), lines.size()); + + int indexFirstLine = lines.size() - sublist.size(); + + return new LinesWithOffset(indexFirstLine, sublist); + }); + + try { + // Ensure we do not wait if the benchmark was cancelled before this thread was ready + if (aborted) { + work.cancel(true); + } + ProgramResult programResult = work.get(); + + addProgramOutput(generalInfo, infoSections, programResult); + + setResult(interpretResult(infoSections, programResult)); + } catch (ExecutionException e) { + setResult(interpretExecutionException(infoSections, e)); + } catch (InterruptedException e) { + work.cancel(true); + infoSections.addSection("Failed", "The benchmark thread was interrupted"); + setResult(failedBenchResult(infoSections)); + } catch (CancellationException e) { + infoSections.addSection("Failed", "The run was aborted"); + setResult(failedBenchResult(infoSections)); + } + } + + /** + * Sets the result if it hasn't already be set. + * + * @param result the result + */ + private void setResult(@Nonnull BenchResult result) { + this.result.compareAndSet(null, result); + finishFuture.complete(result.isSuccess()); + } + + private StreamsProcessOutput startBenchExecution( + NamedRows generalInfo, Path benchScriptPath) { + Instant startTime = Instant.now(); + + String[] calledCommand = { + benchScriptPath.toAbsolutePath().toString(), taskRepoPath.toAbsolutePath().toString() + }; + generalInfo.addEscapedArray("Executed command", calledCommand); + generalInfo.add("Start time", startTime.toString()); + + return new ProgramExecutor(Delays.TIME_TO_KILL.toMillis()).execute(calledCommand); + } + + private void addProgramOutput( + NamedRows generalInfo, NamedSections infoSections, ProgramResult programResult) { + + generalInfo.add("Stop time", Instant.now().toString()); + generalInfo.add("Execution time", programResult.getRuntime().toString()); + generalInfo.add("Exit code", programResult.getExitCode() + ""); + + infoSections.addSection( + "Stdout", programResult.getStdOut().isEmpty() ? "" : programResult.getStdOut()); + infoSections.addSection( + "Stderr", programResult.getStdErr().isEmpty() ? "" : programResult.getStdErr()); + } + + private BenchResult interpretResult(NamedSections infoSections, ProgramResult programResult) { + if (programResult.getExitCode() == 0) { + return interpretZeroExitCode(infoSections, programResult); + } + return interpretFailingExitCode(infoSections, programResult); + } + + private BenchResult interpretZeroExitCode( + NamedSections infoSections, ProgramResult programResult) { + + Either> bareResult; + + try { + bareResult = new BenchmarkScriptOutputParser().parse(programResult.getStdOut()); + } catch (OutputParseException e) { + infoSections.addSection("Invalid output", e.getMessage()); + infoSections.addSection("Reason", "The benchmark script returned invalid output!"); + return failedBenchResult(infoSections); + } + + return successfulBenchResult( + new Result(bareResult.getRight().orElse(null), bareResult.getLeft().orElse(null))); + } + + private BenchResult interpretFailingExitCode( + NamedSections infoSections, ProgramResult programResult) { + + infoSections.addSection( + "Reason", + "The benchmark script terminated with a non-zero exit code" + + " (" + + programResult.getExitCode() + + ")!"); + + LinuxSignal.forExitCode(programResult.getExitCode()) + .ifPresent( + signal -> + infoSections.addSection( + "Exit code interpretation", + "The exit code looks like the linux signal " + + signal.name() + + ".\n" + + "It's signal number is " + + signal.getNumber() + + " and it is caused by '" + + signal.getExplanation() + + "'.")); + + return failedBenchResult(infoSections); + } + + private BenchResult interpretExecutionException( + NamedSections infoSections, ExecutionException e) { + infoSections.addSection("Stacktrace", ExceptionHelper.getStackTrace(e)); + infoSections.addSection("End time", Instant.now().toString()); + infoSections.addSection( + "Reason", "Maybe an internal runner error. Rebenchmarking might solve the problem!"); + return failedBenchResult(infoSections); + } + + private BenchResult successfulBenchResult(Result result) { + return BenchResult.successful(taskId, result, startTime, Instant.now()); + } + + private BenchResult failedBenchResult(NamedSections infoSections) { + return BenchResult.failed(taskId, infoSections.format(), startTime, Instant.now()); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/LinuxSignal.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/LinuxSignal.java index dfeead2cd..a32e7f9c9 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/LinuxSignal.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/LinuxSignal.java @@ -3,84 +3,82 @@ import java.util.Arrays; import java.util.Optional; -/** - * Common linux (posix) signals ans their meanings. - */ +/** Common linux (posix) signals ans their meanings. */ public enum LinuxSignal { - SIGHUP(1, "Hangup"), - SIGINT(2, "Terminal interrupt signal"), - SIGQUIT(3, "Terminal quit signal"), - SIGILL(4, "Illegal instruction"), - SIGTRAP(5, "Trace/breakpoint trap"), - SIGABRT(6, "Process abort signal"), - SIGBUS(7, "Access to an undefined portion of a memory object"), - SIGFPE(8, "Erroneous arithmetic operation"), - SIGKILL(9, "Kill (cannot be caught or ignored"), - SIGUSR1(10, "User-defined signal 1"), - SIGSEGV(11, "Invalid memory reference"), - SIGUSR2(12, "User-defined signal 2"), - SIGPIPE(13, "Write on a pipe with no one to read it"), - SIGALRM(14, "Alarm clock"), - SIGTERM(15, "Termination signal"), - SIGSTKFLT(16, "Coprocessor experienced a stack fault"), - SIGCHLD(17, "Child process terminated, stopped, or continued"), - SIGCONT(18, "Continue executing, if stopped"), - SIGSTOP(19, "Stop executing (cannot be caught or ignored"), - SIGTSTP(20, "Terminal stop signal"), - SIGTTIN(21, "Background process attempting read"), - SIGTTOU(22, "Background process attempting write"), - SIGURG(23, "Out-of-band data is available at a socket"), - SIGXCPU(24, "CPU time limit exceeded"), - SIGXFSZ(25, "File size limit exceeded"), - SIGVTALRM(26, "Virtual timer expired"), - SIGPROF(27, "Profiling timer expired"), - SIGWINCH(28, "Terminal window size changed"), - SIGPOLL(29, "Pollable event"), - SIGPWR(30, "Power failure"), - SIGSYS(31, "Bad system call"); + SIGHUP(1, "Hangup"), + SIGINT(2, "Terminal interrupt signal"), + SIGQUIT(3, "Terminal quit signal"), + SIGILL(4, "Illegal instruction"), + SIGTRAP(5, "Trace/breakpoint trap"), + SIGABRT(6, "Process abort signal"), + SIGBUS(7, "Access to an undefined portion of a memory object"), + SIGFPE(8, "Erroneous arithmetic operation"), + SIGKILL(9, "Kill (cannot be caught or ignored"), + SIGUSR1(10, "User-defined signal 1"), + SIGSEGV(11, "Invalid memory reference"), + SIGUSR2(12, "User-defined signal 2"), + SIGPIPE(13, "Write on a pipe with no one to read it"), + SIGALRM(14, "Alarm clock"), + SIGTERM(15, "Termination signal"), + SIGSTKFLT(16, "Coprocessor experienced a stack fault"), + SIGCHLD(17, "Child process terminated, stopped, or continued"), + SIGCONT(18, "Continue executing, if stopped"), + SIGSTOP(19, "Stop executing (cannot be caught or ignored"), + SIGTSTP(20, "Terminal stop signal"), + SIGTTIN(21, "Background process attempting read"), + SIGTTOU(22, "Background process attempting write"), + SIGURG(23, "Out-of-band data is available at a socket"), + SIGXCPU(24, "CPU time limit exceeded"), + SIGXFSZ(25, "File size limit exceeded"), + SIGVTALRM(26, "Virtual timer expired"), + SIGPROF(27, "Profiling timer expired"), + SIGWINCH(28, "Terminal window size changed"), + SIGPOLL(29, "Pollable event"), + SIGPWR(30, "Power failure"), + SIGSYS(31, "Bad system call"); - private final int number; - private final String explanation; + private final int number; + private final String explanation; - LinuxSignal(int number, String explanation) { - this.number = number; - this.explanation = explanation; - } + LinuxSignal(int number, String explanation) { + this.number = number; + this.explanation = explanation; + } - /** - * @return the signal number - */ - public int getNumber() { - return number; - } + /** + * @return the signal number + */ + public int getNumber() { + return number; + } - /** - * @return a short explanation for this signal - */ - public String getExplanation() { - return explanation; - } + /** + * @return a short explanation for this signal + */ + public String getExplanation() { + return explanation; + } - /** - * Returns the signal for a given number. - * - * @param number the signal number - * @return the signal or empty if none - */ - public static Optional forSignalNumber(int number) { - return Arrays.stream(values()).filter(signal -> signal.getNumber() == number).findFirst(); - } + /** + * Returns the signal for a given number. + * + * @param number the signal number + * @return the signal or empty if none + */ + public static Optional forSignalNumber(int number) { + return Arrays.stream(values()).filter(signal -> signal.getNumber() == number).findFirst(); + } - /** - * Returns the signal for a given exit code, if any. - * - * @param exitCode the exit code - * @return the signal or empty if none - */ - public static Optional forExitCode(int exitCode) { - if (exitCode < 127) { - return Optional.empty(); - } - return forSignalNumber(exitCode - 128); - } + /** + * Returns the signal for a given exit code, if any. + * + * @param exitCode the exit code + * @return the signal or empty if none + */ + public static Optional forExitCode(int exitCode) { + if (exitCode < 127) { + return Optional.empty(); + } + return forSignalNumber(exitCode - 128); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParser.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParser.java index 179b9551c..bc112cff8 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParser.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParser.java @@ -19,174 +19,171 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * The pojo for the output of the benchmark script. - */ +/** The pojo for the output of the benchmark script. */ public class BenchmarkScriptOutputParser { - private static final Logger LOGGER = LoggerFactory.getLogger(BenchmarkScriptOutputParser.class); - - private final ObjectMapper objectMapper = new ObjectMapper() - .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) - .registerModule(new ParameterNamesModule()); - - /** - * Parses the benchmark output node to a {@link Result} object. - * - * @param data the textual data - * @return the parsed benchmark results - * @throws OutputParseException if an error occurs - */ - public Either> parse(String data) throws OutputParseException { - LOGGER.debug("Parsing message '{}'", data); - - JsonNode root; - try { - root = objectMapper.readTree(data); - } catch (JsonProcessingException e) { - throw new OutputParseException(e.getMessage(), e); - } - - if (!root.isObject()) { - throw new OutputParseException("Root is no object"); - } - // the is object check is needed to allow benchmarks named "error" - if (root.hasNonNull("error") && !root.get("error").isObject()) { - if (!root.get("error").isTextual()) { - throw new OutputParseException("Error is no string: " + root); - } - return Either.ofLeft(root.get("error").asText()); - } - - List benchmarks = new ArrayList<>(); - - Iterator> fields = root.fields(); - while (fields.hasNext()) { - Entry field = fields.next(); - benchmarks.add(parseBenchmark(field.getKey(), field.getValue())); - } - - if (benchmarks.isEmpty()) { - throw new OutputParseException("Root element has no benchmarks"); - } - - return Either.ofRight(benchmarks); - } - - private Benchmark parseBenchmark(String name, JsonNode node) { - if (!node.isObject()) { - throw new OutputParseException("benchmark is no object: " + node); - } - - List metrics = new ArrayList<>(); - - Iterator> fields = node.fields(); - while (fields.hasNext()) { - Entry field = fields.next(); - metrics.add(parseMetric(field.getKey(), field.getValue())); - } - - return new Benchmark(name, metrics); - } - - private Metric parseMetric(String name, JsonNode node) { - if (!node.isObject()) { - throw new OutputParseException("Metric is no object: " + node); - } - - Optional unit = Optional.empty(); - if (node.hasNonNull("unit")) { - // parseUnit will throw if the value is invalid - unit = Optional.of(parseUnit(node.get("unit"))); - } - - Optional interpretation = getInterpretationFieldName(node) - .map(node::get) - // parseInterpretation will throw if the value is invalid - .map(this::parseInterpretation); - - if (node.hasNonNull("error")) { - if (!node.get("error").isTextual()) { - throw new OutputParseException("Error is no string: " + node); - } - - return new Metric( - name, node.get("error").asText(), unit.orElse(null), interpretation.orElse(null), null - ); - } else { - String resultsFieldName = getResultsFieldName(node) - .orElseThrow(() -> new OutputParseException("Metric has no results: " + node)); - - return new Metric( - name, null, unit.orElse(null), interpretation.orElse(null), - parseResults(node.get(resultsFieldName)) - ); - } - } - - private Optional getInterpretationFieldName(JsonNode node) { - String fieldName = node.hasNonNull("interpretation") - ? "interpretation" - : "resultInterpretation"; - if (node.hasNonNull(fieldName)) { - return Optional.of(fieldName); - } - return Optional.empty(); - } - - private Optional getResultsFieldName(JsonNode node) { - String fieldName = node.hasNonNull("results") - ? "results" - : "values"; - if (node.hasNonNull(fieldName)) { - return Optional.of(fieldName); - } - return Optional.empty(); - } - - private List parseResults(JsonNode node) { - if (!node.isArray()) { - throw new OutputParseException("Output is no array: " + node); - } - int size = node.size(); - - List results = new ArrayList<>(size); - - ArrayNode arrayNode = (ArrayNode) node; - - for (int i = 0; i < size; i++) { - JsonNode element = arrayNode.get(i); - if (!element.isNumber()) { - throw new OutputParseException( - "Expected a number in: " + node + " at position " + i - ); - } - results.add(element.asDouble()); - } - - if (results.isEmpty()) { - throw new OutputParseException("Expected result to have at least one value!"); - } - - return results; - } - - private String parseUnit(JsonNode node) { - if (!node.isTextual()) { - throw new OutputParseException("Unit is no string: " + node); - } - return node.asText(); - } - - private Interpretation parseInterpretation(JsonNode node) { - if (!node.isTextual()) { - throw new OutputParseException("Interpretation is no string: " + node); - } - - try { - return Interpretation.valueOf(node.asText()); - } catch (IllegalArgumentException e) { - throw new OutputParseException("Unknown interpretation: " + node); - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(BenchmarkScriptOutputParser.class); + + private final ObjectMapper objectMapper = + new ObjectMapper() + .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .registerModule(new ParameterNamesModule()); + + /** + * Parses the benchmark output node to a {@link Result} object. + * + * @param data the textual data + * @return the parsed benchmark results + * @throws OutputParseException if an error occurs + */ + public Either> parse(String data) throws OutputParseException { + LOGGER.debug("Parsing message '{}'", data); + + JsonNode root; + try { + root = objectMapper.readTree(data); + } catch (JsonProcessingException e) { + throw new OutputParseException(e.getMessage(), e); + } + + if (!root.isObject()) { + throw new OutputParseException("Root is no object"); + } + // the is object check is needed to allow benchmarks named "error" + if (root.hasNonNull("error") && !root.get("error").isObject()) { + if (!root.get("error").isTextual()) { + throw new OutputParseException("Error is no string: " + root); + } + return Either.ofLeft(root.get("error").asText()); + } + + List benchmarks = new ArrayList<>(); + + Iterator> fields = root.fields(); + while (fields.hasNext()) { + Entry field = fields.next(); + benchmarks.add(parseBenchmark(field.getKey(), field.getValue())); + } + + if (benchmarks.isEmpty()) { + throw new OutputParseException("Root element has no benchmarks"); + } + + return Either.ofRight(benchmarks); + } + + private Benchmark parseBenchmark(String name, JsonNode node) { + if (!node.isObject()) { + throw new OutputParseException("benchmark is no object: " + node); + } + + List metrics = new ArrayList<>(); + + Iterator> fields = node.fields(); + while (fields.hasNext()) { + Entry field = fields.next(); + metrics.add(parseMetric(field.getKey(), field.getValue())); + } + + return new Benchmark(name, metrics); + } + + private Metric parseMetric(String name, JsonNode node) { + if (!node.isObject()) { + throw new OutputParseException("Metric is no object: " + node); + } + + Optional unit = Optional.empty(); + if (node.hasNonNull("unit")) { + // parseUnit will throw if the value is invalid + unit = Optional.of(parseUnit(node.get("unit"))); + } + + Optional interpretation = + getInterpretationFieldName(node) + .map(node::get) + // parseInterpretation will throw if the value is invalid + .map(this::parseInterpretation); + + if (node.hasNonNull("error")) { + if (!node.get("error").isTextual()) { + throw new OutputParseException("Error is no string: " + node); + } + + return new Metric( + name, node.get("error").asText(), unit.orElse(null), interpretation.orElse(null), null); + } else { + String resultsFieldName = + getResultsFieldName(node) + .orElseThrow(() -> new OutputParseException("Metric has no results: " + node)); + + return new Metric( + name, + null, + unit.orElse(null), + interpretation.orElse(null), + parseResults(node.get(resultsFieldName))); + } + } + + private Optional getInterpretationFieldName(JsonNode node) { + String fieldName = + node.hasNonNull("interpretation") ? "interpretation" : "resultInterpretation"; + if (node.hasNonNull(fieldName)) { + return Optional.of(fieldName); + } + return Optional.empty(); + } + + private Optional getResultsFieldName(JsonNode node) { + String fieldName = node.hasNonNull("results") ? "results" : "values"; + if (node.hasNonNull(fieldName)) { + return Optional.of(fieldName); + } + return Optional.empty(); + } + + private List parseResults(JsonNode node) { + if (!node.isArray()) { + throw new OutputParseException("Output is no array: " + node); + } + int size = node.size(); + + List results = new ArrayList<>(size); + + ArrayNode arrayNode = (ArrayNode) node; + + for (int i = 0; i < size; i++) { + JsonNode element = arrayNode.get(i); + if (!element.isNumber()) { + throw new OutputParseException("Expected a number in: " + node + " at position " + i); + } + results.add(element.asDouble()); + } + + if (results.isEmpty()) { + throw new OutputParseException("Expected result to have at least one value!"); + } + + return results; + } + + private String parseUnit(JsonNode node) { + if (!node.isTextual()) { + throw new OutputParseException("Unit is no string: " + node); + } + return node.asText(); + } + + private Interpretation parseInterpretation(JsonNode node) { + if (!node.isTextual()) { + throw new OutputParseException("Interpretation is no string: " + node); + } + + try { + return Interpretation.valueOf(node.asText()); + } catch (IllegalArgumentException e) { + throw new OutputParseException("Unknown interpretation: " + node); + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/OutputParseException.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/OutputParseException.java index e7ad1955f..9338e2c26 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/OutputParseException.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/benchmarking/output/OutputParseException.java @@ -1,15 +1,13 @@ package de.aaaaaaah.velcom.runner.benchmarking.output; -/** - * An exception detailing an error parsing the script output. - */ +/** An exception detailing an error parsing the script output. */ public class OutputParseException extends RuntimeException { - public OutputParseException(String message) { - super(message); - } + public OutputParseException(String message) { + super(message); + } - public OutputParseException(String message, Throwable cause) { - super(message, cause); - } + public OutputParseException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedRows.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedRows.java index b9a2ace51..635b358a1 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedRows.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedRows.java @@ -6,90 +6,82 @@ import java.util.List; import java.util.stream.Collectors; -/** - * A way to pretty-print multiple rows of information with names. - */ +/** A way to pretty-print multiple rows of information with names. */ public class NamedRows { - private final List rows; - - public NamedRows() { - rows = new ArrayList<>(); - } - - /** - * Add a new row at the bottom. - * - * @param name the row's name - * @param value the row's value - */ - public void add(String name, String value) { - rows.add(new Row(name, value)); - } - - /** - * Add a string as a new row at the bottom, wrapping it in quotes and escaping weird characters. - * - * @param name the row's name - * @param value the string to quote and escape - */ - public void addEscaped(String name, String value) { - add(name, StringHelper.quote(value)); - } - - /** - * Add an array of strings as a new row at the bottom, wrapping the strings in quotes and escaping - * weird characters. - * - * @param name the row's name - * @param array the string array to quote and escape - */ - public void addEscapedArray(String name, String[] array) { - String entries = Arrays.stream(array) - .map(StringHelper::quote) - .collect(Collectors.joining(", ")); - - add(name, "[" + entries + "]"); - } - - /** - * Turn the rows into a string, aligning the values vertically. - * - * @return the rows formatted into a single multiline string - */ - public String format() { - int maxNameLength = rows.stream() - .map(Row::getName) - .map(String::length) - .max(Integer::compareTo) - .orElse(0); - - return rows.stream() - .map(row -> row.format(maxNameLength)) - .collect(Collectors.joining()); - } - - private static class Row { - - private final String name; - private final String value; - - public Row(String name, String value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public String getValue() { - return value; - } - - public String format(int nameWidth) { - // nameWidth + 1 to account for colon after name - return String.format("%-" + (nameWidth + 1) + "s %s\n", (name + ":"), value); - } - } + private final List rows; + + public NamedRows() { + rows = new ArrayList<>(); + } + + /** + * Add a new row at the bottom. + * + * @param name the row's name + * @param value the row's value + */ + public void add(String name, String value) { + rows.add(new Row(name, value)); + } + + /** + * Add a string as a new row at the bottom, wrapping it in quotes and escaping weird characters. + * + * @param name the row's name + * @param value the string to quote and escape + */ + public void addEscaped(String name, String value) { + add(name, StringHelper.quote(value)); + } + + /** + * Add an array of strings as a new row at the bottom, wrapping the strings in quotes and escaping + * weird characters. + * + * @param name the row's name + * @param array the string array to quote and escape + */ + public void addEscapedArray(String name, String[] array) { + String entries = + Arrays.stream(array).map(StringHelper::quote).collect(Collectors.joining(", ")); + + add(name, "[" + entries + "]"); + } + + /** + * Turn the rows into a string, aligning the values vertically. + * + * @return the rows formatted into a single multiline string + */ + public String format() { + int maxNameLength = + rows.stream().map(Row::getName).map(String::length).max(Integer::compareTo).orElse(0); + + return rows.stream().map(row -> row.format(maxNameLength)).collect(Collectors.joining()); + } + + private static class Row { + + private final String name; + private final String value; + + public Row(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public String format(int nameWidth) { + // nameWidth + 1 to account for colon after name + return String.format("%-" + (nameWidth + 1) + "s %s\n", (name + ":"), value); + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedSections.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedSections.java index 0b34a4f0e..94d3a5d6c 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedSections.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/formatting/NamedSections.java @@ -11,98 +11,95 @@ */ public class NamedSections { - private final List

sections; - - public NamedSections() { - sections = new ArrayList<>(); - } - - - /** - * Add a string as a new section after all other sections. - * - * @param name the name of the section - * @param content the content of the section - */ - public void addSection(String name, String content) { - sections.add(new Section(name, Either.ofLeft(content))); - } - - /** - * Add a {@link NamedRows} as a new section after all other sections. Any further changes to the - * {@link NamedRows} object will be reflected in the section contents. - * - * @param name the name of the section - * @param rows the content of the section - */ - public void addSection(String name, NamedRows rows) { - sections.add(new Section(name, Either.ofRight(rows))); - } - - /** - * Add a string as a new section in front of all all other sections. - * - * @param name the name of the section - * @param content the content of the section - */ - public void addSectionInFront(String name, String content) { - sections.add(0, new Section(name, Either.ofLeft(content))); - } - - /** - * Add a {@link NamedRows} as a new section in front of all all other sections. Any further - * changes to the {@link NamedRows} object will be reflected in the section contents. - * - * @param name the name of the section - * @param rows the content of the section - */ - public void addSectionInFront(String name, NamedRows rows) { - sections.add(0, new Section(name, Either.ofRight(rows))); - } - - /** - * Turn the sections into a string. Only at this point are any {@link NamedRows} formatted. - * - * @return the sections formatted into a single multiline string - */ - public String format() { - return sections.stream() - .map(Section::format) - .collect(Collectors.joining("\n")); - } - - private static class Section { - - private final String name; - private final Either content; - - public Section(String name, Either content) { - this.name = name; - this.content = content; - } - - public String getName() { - return name; - } - - public Either getContent() { - return content; - } - - public String format() { - StringBuilder builder = new StringBuilder(); - - builder.append("#".repeat(name.length() + 6)).append("\n"); - builder.append("## ").append(name).append(" ##\n"); - builder.append("#".repeat(name.length() + 6)).append("\n"); - - String contentString = content.consume(s -> s, NamedRows::format); - builder.append(contentString); - if (!contentString.endsWith("\n")) { - builder.append("\n"); - } - - return builder.toString(); - } - } + private final List
sections; + + public NamedSections() { + sections = new ArrayList<>(); + } + + /** + * Add a string as a new section after all other sections. + * + * @param name the name of the section + * @param content the content of the section + */ + public void addSection(String name, String content) { + sections.add(new Section(name, Either.ofLeft(content))); + } + + /** + * Add a {@link NamedRows} as a new section after all other sections. Any further changes to the + * {@link NamedRows} object will be reflected in the section contents. + * + * @param name the name of the section + * @param rows the content of the section + */ + public void addSection(String name, NamedRows rows) { + sections.add(new Section(name, Either.ofRight(rows))); + } + + /** + * Add a string as a new section in front of all all other sections. + * + * @param name the name of the section + * @param content the content of the section + */ + public void addSectionInFront(String name, String content) { + sections.add(0, new Section(name, Either.ofLeft(content))); + } + + /** + * Add a {@link NamedRows} as a new section in front of all all other sections. Any further + * changes to the {@link NamedRows} object will be reflected in the section contents. + * + * @param name the name of the section + * @param rows the content of the section + */ + public void addSectionInFront(String name, NamedRows rows) { + sections.add(0, new Section(name, Either.ofRight(rows))); + } + + /** + * Turn the sections into a string. Only at this point are any {@link NamedRows} formatted. + * + * @return the sections formatted into a single multiline string + */ + public String format() { + return sections.stream().map(Section::format).collect(Collectors.joining("\n")); + } + + private static class Section { + + private final String name; + private final Either content; + + public Section(String name, Either content) { + this.name = name; + this.content = content; + } + + public String getName() { + return name; + } + + public Either getContent() { + return content; + } + + public String format() { + StringBuilder builder = new StringBuilder(); + + builder.append("#".repeat(name.length() + 6)).append("\n"); + builder.append("## ").append(name).append(" ##\n"); + builder.append("#".repeat(name.length() + 6)).append("\n"); + + String contentString = content.consume(s -> s, NamedRows::format); + builder.append(contentString); + if (!contentString.endsWith("\n")) { + builder.append("\n"); + } + + return builder.toString(); + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingBench.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingBench.java index e7d832dd4..4871fe4c9 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingBench.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingBench.java @@ -19,78 +19,81 @@ */ public class AwaitingBench extends RunnerState { - private static final Logger LOGGER = LoggerFactory.getLogger(AwaitingBench.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AwaitingBench.class); - private final RequestRunReply reply; - private final CompletableFuture replyFuture; - private boolean owningReplyFuture; + private final RequestRunReply reply; + private final CompletableFuture replyFuture; + private boolean owningReplyFuture; - private OutputStream tmpFile; + private OutputStream tmpFile; - public AwaitingBench(TeleBackend teleBackend, Connection connection, - RequestRunReply reply, CompletableFuture replyFuture) { + public AwaitingBench( + TeleBackend teleBackend, + Connection connection, + RequestRunReply reply, + CompletableFuture replyFuture) { - super(teleBackend, connection); + super(teleBackend, connection); - this.replyFuture = replyFuture; - owningReplyFuture = true; + this.replyFuture = replyFuture; + owningReplyFuture = true; - this.reply = reply; - } + this.reply = reply; + } - @Override - public void onEnter() { - LOGGER.info("{} - Receiving bench repo", teleBackend.getAddress()); + @Override + public void onEnter() { + LOGGER.info("{} - Receiving bench repo", teleBackend.getAddress()); - try { - Path benchRepoTmpPath = teleBackend.getBenchRepoTmpPath(); - Files.createDirectories(benchRepoTmpPath.getParent()); - tmpFile = Files.newOutputStream(benchRepoTmpPath); - } catch (IOException e) { - LOGGER.warn("{} - Could not open stream to bench repo tmp file", teleBackend.getAddress(), e); - } - } + try { + Path benchRepoTmpPath = teleBackend.getBenchRepoTmpPath(); + Files.createDirectories(benchRepoTmpPath.getParent()); + tmpFile = Files.newOutputStream(benchRepoTmpPath); + } catch (IOException e) { + LOGGER.warn("{} - Could not open stream to bench repo tmp file", teleBackend.getAddress(), e); + } + } - @Override - public RunnerState onBinary(ByteBuffer data, boolean last) { - if (tmpFile != null) { - byte[] bytes = new byte[data.remaining()]; - data.get(bytes); - try { - tmpFile.write(bytes); - } catch (IOException e) { - LOGGER.warn("{} - Could not stream to bench repo tmp file", teleBackend.getAddress(), e); - tmpFile = null; - } - } + @Override + public RunnerState onBinary(ByteBuffer data, boolean last) { + if (tmpFile != null) { + byte[] bytes = new byte[data.remaining()]; + data.get(bytes); + try { + tmpFile.write(bytes); + } catch (IOException e) { + LOGGER.warn("{} - Could not stream to bench repo tmp file", teleBackend.getAddress(), e); + tmpFile = null; + } + } - if (!last) { - return this; - } else if (tmpFile == null) { - return new Idle(teleBackend, connection); - } else if (reply.hasRun()) { - owningReplyFuture = false; - return new AwaitingRun(teleBackend, connection, reply, replyFuture); - } else { - owningReplyFuture = false; - replyFuture.complete(reply); - return new Idle(teleBackend, connection); - } - } + if (!last) { + return this; + } else if (tmpFile == null) { + return new Idle(teleBackend, connection); + } else if (reply.hasRun()) { + owningReplyFuture = false; + return new AwaitingRun(teleBackend, connection, reply, replyFuture); + } else { + owningReplyFuture = false; + replyFuture.complete(reply); + return new Idle(teleBackend, connection); + } + } - @Override - public void onExit() { - if (tmpFile != null) { - try { - tmpFile.close(); - } catch (IOException e) { - LOGGER - .warn("{} - Could not close stream to bench repo tmp file", teleBackend.getAddress(), e); - } - } + @Override + public void onExit() { + if (tmpFile != null) { + try { + tmpFile.close(); + } catch (IOException e) { + LOGGER.warn( + "{} - Could not close stream to bench repo tmp file", teleBackend.getAddress(), e); + } + } - if (owningReplyFuture) { - replyFuture.cancel(true); - } - } + if (owningReplyFuture) { + replyFuture.cancel(true); + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRequestRunReply.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRequestRunReply.java index f38061211..b69e2c80e 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRequestRunReply.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRequestRunReply.java @@ -14,65 +14,70 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Waiting for a {@link RequestRunReply} from the server. - */ +/** Waiting for a {@link RequestRunReply} from the server. */ public class AwaitingRequestRunReply extends RunnerState { - private static final Logger LOGGER = LoggerFactory.getLogger(AwaitingRequestRunReply.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AwaitingRequestRunReply.class); - private final CompletableFuture replyFuture; - private boolean owningReplyFuture; - private final Timeout timeout; + private final CompletableFuture replyFuture; + private boolean owningReplyFuture; + private final Timeout timeout; - public AwaitingRequestRunReply(TeleBackend teleBackend, Connection connection, - CompletableFuture replyFuture) { + public AwaitingRequestRunReply( + TeleBackend teleBackend, + Connection connection, + CompletableFuture replyFuture) { - super(teleBackend, connection); + super(teleBackend, connection); - this.replyFuture = replyFuture; - owningReplyFuture = true; + this.replyFuture = replyFuture; + owningReplyFuture = true; - timeout = Timeout.after(Delays.AWAIT_COMMAND_REPLY); - timeout.getCompletionStage() - .thenAccept(aVoid -> connection.close(StatusCode.COMMAND_TIMEOUT)); - } + timeout = Timeout.after(Delays.AWAIT_COMMAND_REPLY); + timeout.getCompletionStage().thenAccept(aVoid -> connection.close(StatusCode.COMMAND_TIMEOUT)); + } - @Override - public void onEnter() { - LOGGER.debug("{} - Waiting for request_run_reply", teleBackend.getAddress()); - timeout.start(); - } + @Override + public void onEnter() { + LOGGER.debug("{} - Waiting for request_run_reply", teleBackend.getAddress()); + timeout.start(); + } - @Override - protected Optional onPacket(ClientBoundPacket packet) { - Serializer serializer = connection.getSerializer(); + @Override + protected Optional onPacket(ClientBoundPacket packet) { + Serializer serializer = connection.getSerializer(); - return super.onPacket(packet).or(() -> Optional.of(packet) - .filter(p -> p.getType() == ClientBoundPacketType.REQUEST_RUN_REPLY) - .flatMap(p -> serializer.deserialize(p.getData(), RequestRunReply.class)) - .map(p -> { - LOGGER - .debug("{} - hasBench {}, hasRun {}", teleBackend.getAddress(), p.hasBench(), p.hasRun()); - if (p.hasBench()) { - owningReplyFuture = false; - return new AwaitingBench(teleBackend, connection, p, replyFuture); - } else if (p.hasRun()) { - owningReplyFuture = false; - return new AwaitingRun(teleBackend, connection, p, replyFuture); - } else { - return new Idle(teleBackend, connection); - } - }) - ); - } + return super.onPacket(packet) + .or( + () -> + Optional.of(packet) + .filter(p -> p.getType() == ClientBoundPacketType.REQUEST_RUN_REPLY) + .flatMap(p -> serializer.deserialize(p.getData(), RequestRunReply.class)) + .map( + p -> { + LOGGER.debug( + "{} - hasBench {}, hasRun {}", + teleBackend.getAddress(), + p.hasBench(), + p.hasRun()); + if (p.hasBench()) { + owningReplyFuture = false; + return new AwaitingBench(teleBackend, connection, p, replyFuture); + } else if (p.hasRun()) { + owningReplyFuture = false; + return new AwaitingRun(teleBackend, connection, p, replyFuture); + } else { + return new Idle(teleBackend, connection); + } + })); + } - @Override - public void onExit() { - timeout.cancel(); + @Override + public void onExit() { + timeout.cancel(); - if (owningReplyFuture) { - replyFuture.cancel(true); - } - } + if (owningReplyFuture) { + replyFuture.cancel(true); + } + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRun.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRun.java index 561a80bb0..94c06e48a 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRun.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/AwaitingRun.java @@ -18,70 +18,73 @@ */ public class AwaitingRun extends RunnerState { - private static final Logger LOGGER = LoggerFactory.getLogger(AwaitingRun.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AwaitingRun.class); - private final CompletableFuture replyFuture; - private final RequestRunReply reply; + private final CompletableFuture replyFuture; + private final RequestRunReply reply; - private OutputStream tmpFile; + private OutputStream tmpFile; - public AwaitingRun(TeleBackend teleBackend, Connection connection, - RequestRunReply reply, CompletableFuture replyFuture) { + public AwaitingRun( + TeleBackend teleBackend, + Connection connection, + RequestRunReply reply, + CompletableFuture replyFuture) { - super(teleBackend, connection); + super(teleBackend, connection); - this.replyFuture = replyFuture; - this.reply = reply; - } + this.replyFuture = replyFuture; + this.reply = reply; + } - @Override - public void onEnter() { - LOGGER.info("{} - Receiving task repo for task {}", teleBackend.getAddress(), - reply.getRunId().get()); + @Override + public void onEnter() { + LOGGER.info( + "{} - Receiving task repo for task {}", teleBackend.getAddress(), reply.getRunId().get()); - try { - Path taskRepoTmpPath = teleBackend.getTaskRepoTmpPath(); - Files.createDirectories(taskRepoTmpPath.getParent()); - tmpFile = Files.newOutputStream(taskRepoTmpPath); - } catch (IOException e) { - LOGGER.warn("{} - Could not open stream to task repo tmp file", teleBackend.getAddress(), e); - } - } + try { + Path taskRepoTmpPath = teleBackend.getTaskRepoTmpPath(); + Files.createDirectories(taskRepoTmpPath.getParent()); + tmpFile = Files.newOutputStream(taskRepoTmpPath); + } catch (IOException e) { + LOGGER.warn("{} - Could not open stream to task repo tmp file", teleBackend.getAddress(), e); + } + } - @Override - public RunnerState onBinary(ByteBuffer data, boolean last) { - if (tmpFile != null) { - byte[] bytes = new byte[data.remaining()]; - data.get(bytes); - try { - tmpFile.write(bytes); - } catch (IOException e) { - LOGGER.warn("{} - Could not stream to task repo tmp file", teleBackend.getAddress(), e); - tmpFile = null; - } - } + @Override + public RunnerState onBinary(ByteBuffer data, boolean last) { + if (tmpFile != null) { + byte[] bytes = new byte[data.remaining()]; + data.get(bytes); + try { + tmpFile.write(bytes); + } catch (IOException e) { + LOGGER.warn("{} - Could not stream to task repo tmp file", teleBackend.getAddress(), e); + tmpFile = null; + } + } - if (!last) { - return this; - } else if (tmpFile == null) { - return new Idle(teleBackend, connection); - } else { - replyFuture.complete(reply); - return new Idle(teleBackend, connection); - } - } + if (!last) { + return this; + } else if (tmpFile == null) { + return new Idle(teleBackend, connection); + } else { + replyFuture.complete(reply); + return new Idle(teleBackend, connection); + } + } - @Override - public void onExit() { - if (tmpFile != null) { - try { - tmpFile.close(); - } catch (IOException e) { - LOGGER - .warn("{} - Could not close stream to task repo tmp file", teleBackend.getAddress(), e); - } - } + @Override + public void onExit() { + if (tmpFile != null) { + try { + tmpFile.close(); + } catch (IOException e) { + LOGGER.warn( + "{} - Could not close stream to task repo tmp file", teleBackend.getAddress(), e); + } + } - replyFuture.cancel(true); - } + replyFuture.cancel(true); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/Idle.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/Idle.java index ba044aa5e..dfa24fc7a 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/Idle.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/Idle.java @@ -9,12 +9,12 @@ */ public class Idle extends RunnerState { - public Idle(TeleBackend teleBackend, Connection connection) { - super(teleBackend, connection); - } + public Idle(TeleBackend teleBackend, Connection connection) { + super(teleBackend, connection); + } - @Override - public boolean isResting() { - return true; - } + @Override + public boolean isResting() { + return true; + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/RunnerState.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/RunnerState.java index 7d6640acc..9b99ccf0f 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/RunnerState.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/states/RunnerState.java @@ -29,145 +29,147 @@ */ public abstract class RunnerState implements State { - private static final Logger LOGGER = LoggerFactory.getLogger(RunnerState.class); - - protected final TeleBackend teleBackend; - protected final Connection connection; - - public RunnerState(TeleBackend teleBackend, Connection connection) { - this.teleBackend = teleBackend; - this.connection = connection; - } - - /** - * Called when a complete text packet has been received via the websocket connection. - * - * @param text The contents of the received packet - * @return the state to switch to next - */ - public RunnerState onText(String text) { - Optional newState = connection.getSerializer() - .deserialize(text, ClientBoundPacket.class) - .flatMap(this::onPacket); - - // If a packet has been received that could not be deserialized or handled, that is invalid - // behaviour. - if (newState.isEmpty()) { - LOGGER.warn("Received invalid packet, closing connection"); - LOGGER.warn("Packet: {}", text); - connection.close(StatusCode.ILLEGAL_PACKET); - } - - return newState.orElse(this); - } - - /** - * Called by the default {@link #onText(String)} implementation if the text could be decoded to a - * {@link ClientBoundPacket}. - * - * @param packet the packet the text was deserialized into - * @return whether this function call handled the packet - */ - protected Optional onPacket(ClientBoundPacket packet) { - Serializer serializer = connection.getSerializer(); - JsonNode data = packet.getData(); - - switch (packet.getType()) { - case GET_STATUS: - return serializer.deserialize(data, GetStatus.class).flatMap(this::onGetStatus); - - case GET_RESULT: - return serializer.deserialize(data, GetResult.class).flatMap(this::onGetResult); - - case CLEAR_RESULT: - return serializer.deserialize(data, ClearResult.class).flatMap(this::onClearResult); - - case ABORT_RUN: - return serializer.deserialize(data, AbortRun.class).flatMap(this::onAbortRun); - - default: - return Optional.empty(); - } - } - - protected Optional onGetStatus(GetStatus getStatus) { - LOGGER.debug("{} - Replying to get_status", teleBackend.getAddress()); - - GetStatusReply getStatusReply = new GetStatusReply( - teleBackend.getInfo().format(), - GitProperties.getHash(), - teleBackend.getBenchHash().orElse(null), - teleBackend.getBenchResult().isPresent(), - teleBackend.getStatus(), - teleBackend.getCurrentRunId().orElse(null), - teleBackend.getLastOutputLines().orElse(null) - ); - LOGGER.debug("{} - Replying with {}", teleBackend.getAddress(), getStatusReply); - connection.sendPacket(getStatusReply.asPacket(connection.getSerializer())); - - return Optional.of(this); - } - - protected Optional onGetResult(GetResult getResult) { - LOGGER.debug("{} - Replying to get_result", teleBackend.getAddress()); - - Optional resultOptional = teleBackend.getBenchResult(); - if (resultOptional.isEmpty()) { - connection.close(StatusCode.NO_RESULT); - return Optional.empty(); - } - BenchResult result = resultOptional.get(); - LOGGER - .debug("{} - Replying with result for run {}", teleBackend.getAddress(), result.getRunId()); - - GetResultReply getResultReply = new GetResultReply( - result.getRunId(), - result.isSuccess(), - result.getResult().getRight().orElse(null), - result.getResult().getLeft().orElse(null), - result.getStartTime(), - result.getStopTime() - ); - connection.sendPacket(getResultReply.asPacket(connection.getSerializer())); - - return Optional.of(this); - } - - protected Optional onClearResult(ClearResult clearResult) { - LOGGER.debug("{} - Replying to clear_result", teleBackend.getAddress()); - - if (!teleBackend.clearBenchResult()) { - connection.close(StatusCode.NO_RESULT); - return Optional.empty(); - } - connection.sendPacket(new ClearResultReply().asPacket(connection.getSerializer())); - - return Optional.of(this); - } - - protected Optional onAbortRun(AbortRun abortRun) { - LOGGER.debug("{} - Replying to abort_run", teleBackend.getAddress()); - - teleBackend.abortCurrentRun(); - connection.sendPacket(new AbortRunReply().asPacket(connection.getSerializer())); - - return Optional.of(this); - } - - /** - * Called when (part of) a binary packet has been received via the websocket connection. The - * {@link ByteBuffer} must not be used any further after this function returns! - * - * @param data the data that was received - * @param last whether this invocation completes the message - * @return the state to switch to next - */ - public RunnerState onBinary(ByteBuffer data, boolean last) { - LOGGER.debug("{} - Received invalid binary data", teleBackend.getAddress()); - - // Binary packets are only expected in certain circumstances, but usually they are invalid - // behaviour. - connection.forceClose(StatusCode.ILLEGAL_BINARY_PACKET); - return this; - } + private static final Logger LOGGER = LoggerFactory.getLogger(RunnerState.class); + + protected final TeleBackend teleBackend; + protected final Connection connection; + + public RunnerState(TeleBackend teleBackend, Connection connection) { + this.teleBackend = teleBackend; + this.connection = connection; + } + + /** + * Called when a complete text packet has been received via the websocket connection. + * + * @param text The contents of the received packet + * @return the state to switch to next + */ + public RunnerState onText(String text) { + Optional newState = + connection + .getSerializer() + .deserialize(text, ClientBoundPacket.class) + .flatMap(this::onPacket); + + // If a packet has been received that could not be deserialized or handled, that is invalid + // behaviour. + if (newState.isEmpty()) { + LOGGER.warn("Received invalid packet, closing connection"); + LOGGER.warn("Packet: {}", text); + connection.close(StatusCode.ILLEGAL_PACKET); + } + + return newState.orElse(this); + } + + /** + * Called by the default {@link #onText(String)} implementation if the text could be decoded to a + * {@link ClientBoundPacket}. + * + * @param packet the packet the text was deserialized into + * @return whether this function call handled the packet + */ + protected Optional onPacket(ClientBoundPacket packet) { + Serializer serializer = connection.getSerializer(); + JsonNode data = packet.getData(); + + switch (packet.getType()) { + case GET_STATUS: + return serializer.deserialize(data, GetStatus.class).flatMap(this::onGetStatus); + + case GET_RESULT: + return serializer.deserialize(data, GetResult.class).flatMap(this::onGetResult); + + case CLEAR_RESULT: + return serializer.deserialize(data, ClearResult.class).flatMap(this::onClearResult); + + case ABORT_RUN: + return serializer.deserialize(data, AbortRun.class).flatMap(this::onAbortRun); + + default: + return Optional.empty(); + } + } + + protected Optional onGetStatus(GetStatus getStatus) { + LOGGER.debug("{} - Replying to get_status", teleBackend.getAddress()); + + GetStatusReply getStatusReply = + new GetStatusReply( + teleBackend.getInfo().format(), + GitProperties.getHash(), + teleBackend.getBenchHash().orElse(null), + teleBackend.getBenchResult().isPresent(), + teleBackend.getStatus(), + teleBackend.getCurrentRunId().orElse(null), + teleBackend.getLastOutputLines().orElse(null)); + LOGGER.debug("{} - Replying with {}", teleBackend.getAddress(), getStatusReply); + connection.sendPacket(getStatusReply.asPacket(connection.getSerializer())); + + return Optional.of(this); + } + + protected Optional onGetResult(GetResult getResult) { + LOGGER.debug("{} - Replying to get_result", teleBackend.getAddress()); + + Optional resultOptional = teleBackend.getBenchResult(); + if (resultOptional.isEmpty()) { + connection.close(StatusCode.NO_RESULT); + return Optional.empty(); + } + BenchResult result = resultOptional.get(); + LOGGER.debug( + "{} - Replying with result for run {}", teleBackend.getAddress(), result.getRunId()); + + GetResultReply getResultReply = + new GetResultReply( + result.getRunId(), + result.isSuccess(), + result.getResult().getRight().orElse(null), + result.getResult().getLeft().orElse(null), + result.getStartTime(), + result.getStopTime()); + connection.sendPacket(getResultReply.asPacket(connection.getSerializer())); + + return Optional.of(this); + } + + protected Optional onClearResult(ClearResult clearResult) { + LOGGER.debug("{} - Replying to clear_result", teleBackend.getAddress()); + + if (!teleBackend.clearBenchResult()) { + connection.close(StatusCode.NO_RESULT); + return Optional.empty(); + } + connection.sendPacket(new ClearResultReply().asPacket(connection.getSerializer())); + + return Optional.of(this); + } + + protected Optional onAbortRun(AbortRun abortRun) { + LOGGER.debug("{} - Replying to abort_run", teleBackend.getAddress()); + + teleBackend.abortCurrentRun(); + connection.sendPacket(new AbortRunReply().asPacket(connection.getSerializer())); + + return Optional.of(this); + } + + /** + * Called when (part of) a binary packet has been received via the websocket connection. The + * {@link ByteBuffer} must not be used any further after this function returns! + * + * @param data the data that was received + * @param last whether this invocation completes the message + * @return the state to switch to next + */ + public RunnerState onBinary(ByteBuffer data, boolean last) { + LOGGER.debug("{} - Received invalid binary data", teleBackend.getAddress()); + + // Binary packets are only expected in certain circumstances, but usually they are invalid + // behaviour. + connection.forceClose(StatusCode.ILLEGAL_BINARY_PACKET); + return this; + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDir.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDir.java index 637e8e111..65805dacb 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDir.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDir.java @@ -15,84 +15,81 @@ /** * This class manages the directory where the bench repo is stored, as well as the file containing - * the bench repo's hash and a temporary file used while downloading and unpacking a new bench - * repo. + * the bench repo's hash and a temporary file used while downloading and unpacking a new bench repo. */ public class BenchRepoDir { - private static final Logger LOGGER = LoggerFactory.getLogger(BenchRepoDir.class); - - private final Path dirPath; - private final Path tmpFilePath; - private final Path hashFilePath; - - @Nullable - private String currentHash; - - public BenchRepoDir(Path dirPath) throws IOException { - this.dirPath = dirPath; - this.tmpFilePath = dirPath.getParent().resolve(dirPath.getFileName() + ".tmp"); - this.hashFilePath = dirPath.getParent().resolve(dirPath.getFileName() + ".hash"); - - currentHash = readHash(); - } - - @Nullable - private String readHash() throws IOException { - try { - LOGGER.debug("Trying to read hash from file {}", hashFilePath); - FileReader reader = new FileReader(hashFilePath.toFile()); - BufferedReader bufferedReader = new BufferedReader(reader); - String line = bufferedReader.readLine(); - if (line == null) { - LOGGER.debug("Could not read hash, file was empty"); - return null; - } - - String hash = line.strip(); - LOGGER.debug("Read hash {}", hash); - return hash; - } catch (FileNotFoundException e) { - LOGGER.debug("Could not read hash, file not found"); - return null; - } - } - - private void writeHash(@Nullable String hash) throws IOException { - if (hash == null) { - Files.deleteIfExists(hashFilePath); - return; - } - FileWriter writer = new FileWriter(hashFilePath.toFile()); - writer.write(hash); - writer.flush(); - } - - /** - * Delete the directory and hash file. - * - * @throws IOException if something io-related goes wrong during the deletion - */ - public void clear() throws IOException { - setHash(null); - FileHelper.deleteDirectoryOrFile(dirPath); - } - - public Path getDirPath() { - return dirPath; - } - - public Path getTmpFilePath() { - return tmpFilePath; - } - - public void setHash(@Nullable String hash) throws IOException { - currentHash = hash; - writeHash(currentHash); - } - - public Optional getHash() { - return Optional.ofNullable(currentHash); - } - + private static final Logger LOGGER = LoggerFactory.getLogger(BenchRepoDir.class); + + private final Path dirPath; + private final Path tmpFilePath; + private final Path hashFilePath; + + @Nullable private String currentHash; + + public BenchRepoDir(Path dirPath) throws IOException { + this.dirPath = dirPath; + this.tmpFilePath = dirPath.getParent().resolve(dirPath.getFileName() + ".tmp"); + this.hashFilePath = dirPath.getParent().resolve(dirPath.getFileName() + ".hash"); + + currentHash = readHash(); + } + + @Nullable + private String readHash() throws IOException { + try { + LOGGER.debug("Trying to read hash from file {}", hashFilePath); + FileReader reader = new FileReader(hashFilePath.toFile()); + BufferedReader bufferedReader = new BufferedReader(reader); + String line = bufferedReader.readLine(); + if (line == null) { + LOGGER.debug("Could not read hash, file was empty"); + return null; + } + + String hash = line.strip(); + LOGGER.debug("Read hash {}", hash); + return hash; + } catch (FileNotFoundException e) { + LOGGER.debug("Could not read hash, file not found"); + return null; + } + } + + private void writeHash(@Nullable String hash) throws IOException { + if (hash == null) { + Files.deleteIfExists(hashFilePath); + return; + } + FileWriter writer = new FileWriter(hashFilePath.toFile()); + writer.write(hash); + writer.flush(); + } + + /** + * Delete the directory and hash file. + * + * @throws IOException if something io-related goes wrong during the deletion + */ + public void clear() throws IOException { + setHash(null); + FileHelper.deleteDirectoryOrFile(dirPath); + } + + public Path getDirPath() { + return dirPath; + } + + public Path getTmpFilePath() { + return tmpFilePath; + } + + public void setHash(@Nullable String hash) throws IOException { + currentHash = hash; + writeHash(currentHash); + } + + public Optional getHash() { + return Optional.ofNullable(currentHash); + } } diff --git a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/TaskRepoDir.java b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/TaskRepoDir.java index ee5ee0d40..e5e5694f3 100644 --- a/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/TaskRepoDir.java +++ b/backend/runner/src/main/java/de/aaaaaaah/velcom/runner/tmpdirs/TaskRepoDir.java @@ -10,28 +10,28 @@ */ public class TaskRepoDir { - private final Path dirPath; - private final Path tmpFilePath; + private final Path dirPath; + private final Path tmpFilePath; - public TaskRepoDir(Path dirPath) { - this.dirPath = dirPath; - tmpFilePath = dirPath.getParent().resolve(dirPath.getFileName() + ".tmp"); - } + public TaskRepoDir(Path dirPath) { + this.dirPath = dirPath; + tmpFilePath = dirPath.getParent().resolve(dirPath.getFileName() + ".tmp"); + } - /** - * Delete the directory. - * - * @throws IOException if something io-related goes wrong during the deletion - */ - public void clear() throws IOException { - FileHelper.deleteDirectoryOrFile(dirPath); - } + /** + * Delete the directory. + * + * @throws IOException if something io-related goes wrong during the deletion + */ + public void clear() throws IOException { + FileHelper.deleteDirectoryOrFile(dirPath); + } - public Path getDirPath() { - return dirPath; - } + public Path getDirPath() { + return dirPath; + } - public Path getTmpFilePath() { - return tmpFilePath; - } + public Path getTmpFilePath() { + return tmpFilePath; + } } diff --git a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/GitPropertiesTest.java b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/GitPropertiesTest.java index 911487a7e..e178e698c 100644 --- a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/GitPropertiesTest.java +++ b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/GitPropertiesTest.java @@ -8,16 +8,16 @@ class GitPropertiesTest { - @Test - void fieldsPresent() { - Assertions.assertThat(GitProperties.getHash()).isNotBlank(); - assertThat(GitProperties.getHashAbbrev()).isNotBlank(); - assertThat(GitProperties.getBuildTime()).isNotBlank(); - assertThat(GitProperties.getVersion()).isNotBlank(); - } + @Test + void fieldsPresent() { + Assertions.assertThat(GitProperties.getHash()).isNotBlank(); + assertThat(GitProperties.getHashAbbrev()).isNotBlank(); + assertThat(GitProperties.getBuildTime()).isNotBlank(); + assertThat(GitProperties.getVersion()).isNotBlank(); + } - @Test - void hasAbbreviationIsAbbreviation() { - assertThat(GitProperties.getHash()).startsWith(GitProperties.getHashAbbrev()); - } + @Test + void hasAbbreviationIsAbbreviation() { + assertThat(GitProperties.getHash()).startsWith(GitProperties.getHashAbbrev()); + } } diff --git a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/BenchmarkerTest.java b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/BenchmarkerTest.java index 6c9d7c59b..67561e2ae 100644 --- a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/BenchmarkerTest.java +++ b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/BenchmarkerTest.java @@ -31,314 +31,309 @@ class BenchmarkerTest { - public static final String BENCH_REPO_HASH = "hello"; - public static final String RUNNER_NAME = "runner name"; - @TempDir - Path rootTempDir; - Path benchRepoPath; - Path workPath; - private CompletableFuture finishFuture; - - private UUID taskId; - private LinuxSystemInfo systemInfo; - private Instant startTime; - - @BeforeEach - void setUp() throws IOException { - benchRepoPath = rootTempDir.resolve("benchrepo"); - workPath = rootTempDir.resolve("work"); - Files.createDirectory(benchRepoPath); - Files.createDirectory(workPath); - finishFuture = new CompletableFuture<>(); - - taskId = UUID.randomUUID(); - systemInfo = LinuxSystemInfo.getCurrent(); - startTime = Instant.now(); - } - - @Test - void testBenchDoesNotExist() throws Exception { - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getLeft()).isPresent(); - String error = result.getResult().getLeft().get(); - assertThat(error).containsIgnoringCase("not found"); - }, - false - ); - } - - @Test - void testBenchNotReadable() throws Exception { - Path benchScriptPath = benchRepoPath.resolve("bench"); - - Files.createFile(benchScriptPath); - EnumSet permissions = EnumSet.copyOf( - Files.getPosixFilePermissions(benchScriptPath) - ); - permissions.remove(PosixFilePermission.OTHERS_READ); - permissions.remove(PosixFilePermission.OWNER_READ); - permissions.remove(PosixFilePermission.GROUP_READ); - permissions.add(PosixFilePermission.OWNER_EXECUTE); - Files.setPosixFilePermissions(benchScriptPath, permissions); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getLeft()).isPresent(); - String error = result.getResult().getLeft().get(); - assertThat(error).containsIgnoringCase("readable"); - }, - false - ); - } - - @Test - void testBenchNotExecutable() throws Exception { - Path benchScriptPath = benchRepoPath.resolve("bench"); - - Files.createFile(benchScriptPath); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getLeft()).isPresent(); - String error = result.getResult().getLeft().get(); - assertThat(error).containsIgnoringCase("executable"); - }, - false - ); - } - - @Test - void testBenchScriptInvalidOutput() throws Exception { - writeBenchScript( - "#!/bin/sh", - "echo 'Hello'" - ); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getLeft()).isPresent(); - String error = result.getResult().getLeft().get(); - assertThat(error).containsIgnoringCase("invalid"); - assertThat(error).containsIgnoringCase("output"); - }, - false - ); - } - - @Test - void testBenchScriptExitCodeFailure() throws Exception { - writeBenchScript( - "#!/bin/sh", - "exit 1" - ); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getLeft()).isPresent(); - String error = result.getResult().getLeft().get(); - assertThat(error).containsIgnoringCase("exit code"); - assertThat(error).containsIgnoringCase("1"); - }, - false - ); - } - - @Test - void testBenchScriptSignalCodeInterpretation() throws Exception { - writeBenchScript( - "#!/bin/sh", - "exit 130" - ); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getLeft()).isPresent(); - String error = result.getResult().getLeft().get(); - assertThat(error).containsIgnoringCase("exit code"); - assertThat(error).containsIgnoringCase("130"); - assertThat(error).containsIgnoringCase("SIGINT"); - assertThat(error).containsIgnoringCase("signal"); - assertThat(error).containsIgnoringCase("Terminal interrupt"); - }, - false - ); - } - - @Test - void testBenchScriptBenchmarkError() throws Exception { - writeBenchScript( - "#!/bin/sh", - "echo '{ \"error\": \"Halloooo\" }'" - ); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getRight()).isPresent(); - Result success = result.getResult().getRight().get(); - assertThat(success.getBenchmarks()).isEmpty(); - assertThat(success.getError()).isPresent(); - assertThat(success.getError().get()).isEqualTo("Halloooo"); - }, - true - ); - } - - @Test - void testBenchScriptMetricError() throws Exception { - writeBenchScript( - "#!/bin/sh", - "echo '{ \"test\": { \"metric\": { \"error\": \"20\" } } }'" - ); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getRight()).isPresent(); - Result success = result.getResult().getRight().get(); - assertThat(success.getBenchmarks()).isPresent(); - assertThat(success.getBenchmarks().get()).containsExactly(new Benchmark( - "test", - List.of( - new Metric("metric", "20", null, null, null) - ) - )); - assertThat(success.getError()).isEmpty(); - }, - true - ); - } - - @Test - void testBenchScriptMetricWithValue() throws Exception { - writeBenchScript( - "#!/bin/sh", - "echo '{ \"test\": { \"metric\": {" - + " \"unit\": \"cats\", \"resultInterpretation\": \"NEUTRAL\", \"results\": [20, 5]" - + " } } }'" - ); - - doWithResult( - result -> { - assertThat(result.getRunId()).isEqualTo(taskId); - - assertThat(result.getResult().getRight()).isPresent(); - Result success = result.getResult().getRight().get(); - assertThat(success.getBenchmarks()).isPresent(); - assertThat(success.getBenchmarks().get()).containsExactly(new Benchmark( - "test", - List.of( - new Metric("metric", null, "cats", Interpretation.NEUTRAL, List.of(20d, 5d)) - ) - )); - assertThat(success.getError()).isEmpty(); - }, - true - ); - } - - @Test - void testAbort() throws Exception { - writeBenchScript( - "#!/bin/sh", - "while true ; do sleep 1 ; done" - ); - - Benchmarker benchmarker = new Benchmarker(finishFuture, taskId, workPath, BENCH_REPO_HASH, - benchRepoPath, startTime, RUNNER_NAME, systemInfo); - - Thread.sleep(400); - benchmarker.abort(); - - assertThat(finishFuture).succeedsWithin(Duration.ofSeconds(3)); - assertThat(finishFuture.get()).isFalse(); - - assertThat(benchmarker.getResult()).isNotEmpty(); - assertThat(benchmarker.getTaskId()).isEqualTo(taskId); - } - - @Test - void testAbortAfterLongTime() throws Exception { - writeBenchScript( - "#!/bin/sh", - "while true ; do sleep 1 ; done" - ); - - Benchmarker benchmarker = new Benchmarker(finishFuture, taskId, workPath, BENCH_REPO_HASH, - benchRepoPath, startTime, RUNNER_NAME, systemInfo); - - Thread.sleep(1000); - benchmarker.abort(); - - assertThat(finishFuture).succeedsWithin(Duration.ofSeconds(3)); - assertThat(finishFuture.get()).isFalse(); - - assertThat(benchmarker.getResult()).isNotEmpty(); - assertThat(benchmarker.getTaskId()).isEqualTo(taskId); - } - - @Test - void abortBeforeStarting() - throws IOException, InterruptedException, ExecutionException, TimeoutException { - writeBenchScript("#!bin/sh", "echo 'hey'"); - - Benchmarker benchmarker = new Benchmarker(finishFuture, taskId, workPath, BENCH_REPO_HASH, - benchRepoPath, startTime, RUNNER_NAME, systemInfo); - - benchmarker.abort(); - - Boolean success = finishFuture.get(10, TimeUnit.SECONDS); - assertThat(success).isFalse(); - - Optional result = benchmarker.getResult(); - - assertThat(result).isPresent(); - assertThat(result.get().isSuccess()).isFalse(); - assertThat(result.get().getResult().getLeft()).get().asInstanceOf(STRING).contains("interrupt"); - assertThat(result.get().getResult().getRight()).isEmpty(); - } - - private void doWithResult(Consumer resultConsumer, Boolean expectedSuccess) - throws InterruptedException, ExecutionException, TimeoutException { - - Benchmarker benchmarker = new Benchmarker(finishFuture, taskId, workPath, BENCH_REPO_HASH, - benchRepoPath, startTime, RUNNER_NAME, systemInfo); - - Boolean success = finishFuture.get(20, TimeUnit.SECONDS); - assertThat(success).isEqualTo(expectedSuccess); - - Optional resultOptional = benchmarker.getResult(); - - assertThat(resultOptional).isPresent(); - - BenchResult result = resultOptional.get(); - resultConsumer.accept(result); - } - - private void writeBenchScript(String... lines) throws IOException { - Path path = benchRepoPath.resolve("bench"); - Files.write(path, Arrays.asList(lines)); - - Set permissions = EnumSet.copyOf( - Files.getPosixFilePermissions(path) - ); - permissions.add(PosixFilePermission.OWNER_EXECUTE); - - Files.setPosixFilePermissions(path, permissions); - } + public static final String BENCH_REPO_HASH = "hello"; + public static final String RUNNER_NAME = "runner name"; + @TempDir Path rootTempDir; + Path benchRepoPath; + Path workPath; + private CompletableFuture finishFuture; + + private UUID taskId; + private LinuxSystemInfo systemInfo; + private Instant startTime; + + @BeforeEach + void setUp() throws IOException { + benchRepoPath = rootTempDir.resolve("benchrepo"); + workPath = rootTempDir.resolve("work"); + Files.createDirectory(benchRepoPath); + Files.createDirectory(workPath); + finishFuture = new CompletableFuture<>(); + + taskId = UUID.randomUUID(); + systemInfo = LinuxSystemInfo.getCurrent(); + startTime = Instant.now(); + } + + @Test + void testBenchDoesNotExist() throws Exception { + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getLeft()).isPresent(); + String error = result.getResult().getLeft().get(); + assertThat(error).containsIgnoringCase("not found"); + }, + false); + } + + @Test + void testBenchNotReadable() throws Exception { + Path benchScriptPath = benchRepoPath.resolve("bench"); + + Files.createFile(benchScriptPath); + EnumSet permissions = + EnumSet.copyOf(Files.getPosixFilePermissions(benchScriptPath)); + permissions.remove(PosixFilePermission.OTHERS_READ); + permissions.remove(PosixFilePermission.OWNER_READ); + permissions.remove(PosixFilePermission.GROUP_READ); + permissions.add(PosixFilePermission.OWNER_EXECUTE); + Files.setPosixFilePermissions(benchScriptPath, permissions); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getLeft()).isPresent(); + String error = result.getResult().getLeft().get(); + assertThat(error).containsIgnoringCase("readable"); + }, + false); + } + + @Test + void testBenchNotExecutable() throws Exception { + Path benchScriptPath = benchRepoPath.resolve("bench"); + + Files.createFile(benchScriptPath); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getLeft()).isPresent(); + String error = result.getResult().getLeft().get(); + assertThat(error).containsIgnoringCase("executable"); + }, + false); + } + + @Test + void testBenchScriptInvalidOutput() throws Exception { + writeBenchScript("#!/bin/sh", "echo 'Hello'"); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getLeft()).isPresent(); + String error = result.getResult().getLeft().get(); + assertThat(error).containsIgnoringCase("invalid"); + assertThat(error).containsIgnoringCase("output"); + }, + false); + } + + @Test + void testBenchScriptExitCodeFailure() throws Exception { + writeBenchScript("#!/bin/sh", "exit 1"); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getLeft()).isPresent(); + String error = result.getResult().getLeft().get(); + assertThat(error).containsIgnoringCase("exit code"); + assertThat(error).containsIgnoringCase("1"); + }, + false); + } + + @Test + void testBenchScriptSignalCodeInterpretation() throws Exception { + writeBenchScript("#!/bin/sh", "exit 130"); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getLeft()).isPresent(); + String error = result.getResult().getLeft().get(); + assertThat(error).containsIgnoringCase("exit code"); + assertThat(error).containsIgnoringCase("130"); + assertThat(error).containsIgnoringCase("SIGINT"); + assertThat(error).containsIgnoringCase("signal"); + assertThat(error).containsIgnoringCase("Terminal interrupt"); + }, + false); + } + + @Test + void testBenchScriptBenchmarkError() throws Exception { + writeBenchScript("#!/bin/sh", "echo '{ \"error\": \"Halloooo\" }'"); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getRight()).isPresent(); + Result success = result.getResult().getRight().get(); + assertThat(success.getBenchmarks()).isEmpty(); + assertThat(success.getError()).isPresent(); + assertThat(success.getError().get()).isEqualTo("Halloooo"); + }, + true); + } + + @Test + void testBenchScriptMetricError() throws Exception { + writeBenchScript("#!/bin/sh", "echo '{ \"test\": { \"metric\": { \"error\": \"20\" } } }'"); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getRight()).isPresent(); + Result success = result.getResult().getRight().get(); + assertThat(success.getBenchmarks()).isPresent(); + assertThat(success.getBenchmarks().get()) + .containsExactly( + new Benchmark("test", List.of(new Metric("metric", "20", null, null, null)))); + assertThat(success.getError()).isEmpty(); + }, + true); + } + + @Test + void testBenchScriptMetricWithValue() throws Exception { + writeBenchScript( + "#!/bin/sh", + "echo '{ \"test\": { \"metric\": {" + + " \"unit\": \"cats\", \"resultInterpretation\": \"NEUTRAL\", \"results\": [20, 5]" + + " } } }'"); + + doWithResult( + result -> { + assertThat(result.getRunId()).isEqualTo(taskId); + + assertThat(result.getResult().getRight()).isPresent(); + Result success = result.getResult().getRight().get(); + assertThat(success.getBenchmarks()).isPresent(); + assertThat(success.getBenchmarks().get()) + .containsExactly( + new Benchmark( + "test", + List.of( + new Metric( + "metric", null, "cats", Interpretation.NEUTRAL, List.of(20d, 5d))))); + assertThat(success.getError()).isEmpty(); + }, + true); + } + + @Test + void testAbort() throws Exception { + writeBenchScript("#!/bin/sh", "while true ; do sleep 1 ; done"); + + Benchmarker benchmarker = + new Benchmarker( + finishFuture, + taskId, + workPath, + BENCH_REPO_HASH, + benchRepoPath, + startTime, + RUNNER_NAME, + systemInfo); + + Thread.sleep(400); + benchmarker.abort(); + + assertThat(finishFuture).succeedsWithin(Duration.ofSeconds(3)); + assertThat(finishFuture.get()).isFalse(); + + assertThat(benchmarker.getResult()).isNotEmpty(); + assertThat(benchmarker.getTaskId()).isEqualTo(taskId); + } + + @Test + void testAbortAfterLongTime() throws Exception { + writeBenchScript("#!/bin/sh", "while true ; do sleep 1 ; done"); + + Benchmarker benchmarker = + new Benchmarker( + finishFuture, + taskId, + workPath, + BENCH_REPO_HASH, + benchRepoPath, + startTime, + RUNNER_NAME, + systemInfo); + + Thread.sleep(1000); + benchmarker.abort(); + + assertThat(finishFuture).succeedsWithin(Duration.ofSeconds(3)); + assertThat(finishFuture.get()).isFalse(); + + assertThat(benchmarker.getResult()).isNotEmpty(); + assertThat(benchmarker.getTaskId()).isEqualTo(taskId); + } + + @Test + void abortBeforeStarting() + throws IOException, InterruptedException, ExecutionException, TimeoutException { + writeBenchScript("#!bin/sh", "echo 'hey'"); + + Benchmarker benchmarker = + new Benchmarker( + finishFuture, + taskId, + workPath, + BENCH_REPO_HASH, + benchRepoPath, + startTime, + RUNNER_NAME, + systemInfo); + + benchmarker.abort(); + + Boolean success = finishFuture.get(10, TimeUnit.SECONDS); + assertThat(success).isFalse(); + + Optional result = benchmarker.getResult(); + + assertThat(result).isPresent(); + assertThat(result.get().isSuccess()).isFalse(); + assertThat(result.get().getResult().getLeft()).get().asInstanceOf(STRING).contains("interrupt"); + assertThat(result.get().getResult().getRight()).isEmpty(); + } + + private void doWithResult(Consumer resultConsumer, Boolean expectedSuccess) + throws InterruptedException, ExecutionException, TimeoutException { + + Benchmarker benchmarker = + new Benchmarker( + finishFuture, + taskId, + workPath, + BENCH_REPO_HASH, + benchRepoPath, + startTime, + RUNNER_NAME, + systemInfo); + + Boolean success = finishFuture.get(20, TimeUnit.SECONDS); + assertThat(success).isEqualTo(expectedSuccess); + + Optional resultOptional = benchmarker.getResult(); + + assertThat(resultOptional).isPresent(); + + BenchResult result = resultOptional.get(); + resultConsumer.accept(result); + } + + private void writeBenchScript(String... lines) throws IOException { + Path path = benchRepoPath.resolve("bench"); + Files.write(path, Arrays.asList(lines)); + + Set permissions = EnumSet.copyOf(Files.getPosixFilePermissions(path)); + permissions.add(PosixFilePermission.OWNER_EXECUTE); + + Files.setPosixFilePermissions(path, permissions); + } } diff --git a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParserTest.java b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParserTest.java index 2ca287957..e713fb12c 100644 --- a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParserTest.java +++ b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/benchmarking/output/BenchmarkScriptOutputParserTest.java @@ -16,155 +16,175 @@ class BenchmarkScriptOutputParserTest { - private BenchmarkScriptOutputParser parser; - - @BeforeEach - void setUp() { - parser = new BenchmarkScriptOutputParser(); - } - - @ParameterizedTest - @CsvFileSource(resources = {"/benchmark-script/valid-input.csv"}, delimiter = '|') - void parseSomeValidInput(boolean error, String line) { - Either> result = parser.parse(line); - if (error) { - assertThat(result.isLeft()).isTrue(); - } else { - assertThat(result.isRight()).isTrue(); - } - } - - @Test - void parseInvalidJson() { - assertThatThrownBy(() -> parser.parse("{ hey")) - .isInstanceOf(OutputParseException.class); - } - - @ParameterizedTest - @CsvSource(value = { - "{\"benchmark\": [] }", - "{\"benchmark\": 20 }", - "{\"benchmark\": false }", - "{\"benchmark\": \"hey\" }", - "{\"benchmark\": null }", - }, delimiter = '|') - void parseSemanticallyInvalidJson(String line) { - assertThatThrownBy(() -> parser.parse(line)) - .isInstanceOf(OutputParseException.class); - } - - @Test - void parseMetricWithErrorAndOtherStuff() { - Either> result = parser.parse( - "{ \"benchmarks\": { \"test\": { \"unit\": \"cats\", \"error\": \"Hey\" } } }" - ); - - assertThat(result.getRight()).isPresent(); - Benchmark benchmark = result.getRight().get().get(0); - assertThat(benchmark.getName()).isEqualTo("benchmarks"); - - assertThat(benchmark.getMetrics()).isNotEmpty(); - Metric metric = benchmark.getMetrics().get(0); - assertThat(metric.getName()).isEqualTo("test"); - assertThat(metric.getError()).isPresent(); - assertThat(metric.getError().get()).isEqualTo("Hey"); - } - - @Test - void parseMetricWithInvalidInterpretation() { - String data = "{ \"test\": { \"metric\": { \"results\": [ 1341173530.8587837 ]," - + " \"unit\": \"hope's\", \"resultInterpretation\": \"NOT VALID\" } } }"; - assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); - } - - @ParameterizedTest - @CsvSource(value = { - "{ \"test\": { \"metric\": { \"error\": 20 } } }", - "{ \"test\": { \"metric\": { \"error\": false } } }", - "{ \"test\": { \"metric\": { \"error\": {} } } }", - "{ \"test\": { \"metric\": { \"error\": [] } } }", - "{ \"error\": 20 }", - "{ \"error\": false }", - "{ \"error\": [] }", - }, delimiter = '|') - void parseInvalidErrorMessage(String data) { - assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); - } - - @ParameterizedTest - @CsvFileSource(resources = {"/benchmark-script/invalid-metric-input.csv"}, delimiter = '|') - void parseInvalidMetric(String data) { - assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); - } - - @ParameterizedTest - @CsvSource(value = { - "{}", - "[]", - "\"hey\"", - "20", - "false", - "null", - }, delimiter = '|') - void parseInvalidRootObjects(String data) { - assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); - } - - @Test - void parseMetricCalledError() { - Either> result = parser.parse( - "{ \"error\": { \"test\": { \"unit\": \"cats\", \"error\": \"Hey\" } } }" - ); - - assertThat(result.getRight()).isPresent(); - Benchmark benchmark = result.getRight().get().get(0); - assertThat(benchmark.getName()).isEqualTo("error"); - - assertThat(benchmark.getMetrics()).isNotEmpty(); - Metric metric = benchmark.getMetrics().get(0); - assertThat(metric.getName()).isEqualTo("test"); - assertThat(metric.getError()).isPresent(); - assertThat(metric.getError().get()).isEqualTo("Hey"); - } - - @Test - void parseInvalidInterpretationInErrorMessage() { - assertThatThrownBy(() -> parser.parse( - "{ \"test\": { \"metric\": { \"error\": \"Yes\", \"interpretation\": \"WHO KNOWS\" } } }" - )) - .isInstanceOf(OutputParseException.class) - .hasMessageContaining("WHO KNOWS"); - } - - @Test - void parseInvalidUnitInErrorMessage() { - assertThatThrownBy(() -> parser.parse( - "{ \"test\": { \"metric\": { \"error\": \"Yes\", \"unit\": 42 } } }" - )) - .isInstanceOf(OutputParseException.class) - .hasMessageContaining("42"); - } - - @Test - void parsedMixOldNew() { - // Old values, new interpretation - assertThat(parser.parse( - "{ \"test\": { \"metric\": { \"values\": [1,2], \"unit\": \"unit\"," - + " \"interpretation\": \"NEUTRAL\" } } }" - )) - .isEqualTo(Either.ofRight(List.of(new Benchmark( - "test", - List.of(new Metric("metric", null, "unit", Interpretation.NEUTRAL, List.of(1.0, 2.0))) - )))); - - // new results, old resultInterpretation - assertThat(parser.parse( - "{ \"test\": { \"metric\": { \"results\": [1,2], \"unit\": \"unit\"," - + " \"resultInterpretation\": \"NEUTRAL\" } } }" - )) - .isEqualTo(Either.ofRight(List.of(new Benchmark( - "test", - List.of(new Metric("metric", null, "unit", Interpretation.NEUTRAL, List.of(1.0, 2.0))) - )))); - } + private BenchmarkScriptOutputParser parser; + + @BeforeEach + void setUp() { + parser = new BenchmarkScriptOutputParser(); + } + + @ParameterizedTest + @CsvFileSource( + resources = {"/benchmark-script/valid-input.csv"}, + delimiter = '|') + void parseSomeValidInput(boolean error, String line) { + Either> result = parser.parse(line); + if (error) { + assertThat(result.isLeft()).isTrue(); + } else { + assertThat(result.isRight()).isTrue(); + } + } + + @Test + void parseInvalidJson() { + assertThatThrownBy(() -> parser.parse("{ hey")).isInstanceOf(OutputParseException.class); + } + + @ParameterizedTest + @CsvSource( + value = { + "{\"benchmark\": [] }", + "{\"benchmark\": 20 }", + "{\"benchmark\": false }", + "{\"benchmark\": \"hey\" }", + "{\"benchmark\": null }", + }, + delimiter = '|') + void parseSemanticallyInvalidJson(String line) { + assertThatThrownBy(() -> parser.parse(line)).isInstanceOf(OutputParseException.class); + } + + @Test + void parseMetricWithErrorAndOtherStuff() { + Either> result = + parser.parse( + "{ \"benchmarks\": { \"test\": { \"unit\": \"cats\", \"error\": \"Hey\" } } }"); + + assertThat(result.getRight()).isPresent(); + Benchmark benchmark = result.getRight().get().get(0); + assertThat(benchmark.getName()).isEqualTo("benchmarks"); + + assertThat(benchmark.getMetrics()).isNotEmpty(); + Metric metric = benchmark.getMetrics().get(0); + assertThat(metric.getName()).isEqualTo("test"); + assertThat(metric.getError()).isPresent(); + assertThat(metric.getError().get()).isEqualTo("Hey"); + } + + @Test + void parseMetricWithInvalidInterpretation() { + String data = + "{ \"test\": { \"metric\": { \"results\": [ 1341173530.8587837 ]," + + " \"unit\": \"hope's\", \"resultInterpretation\": \"NOT VALID\" } } }"; + assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); + } + + @ParameterizedTest + @CsvSource( + value = { + "{ \"test\": { \"metric\": { \"error\": 20 } } }", + "{ \"test\": { \"metric\": { \"error\": false } } }", + "{ \"test\": { \"metric\": { \"error\": {} } } }", + "{ \"test\": { \"metric\": { \"error\": [] } } }", + "{ \"error\": 20 }", + "{ \"error\": false }", + "{ \"error\": [] }", + }, + delimiter = '|') + void parseInvalidErrorMessage(String data) { + assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); + } + + @ParameterizedTest + @CsvFileSource( + resources = {"/benchmark-script/invalid-metric-input.csv"}, + delimiter = '|') + void parseInvalidMetric(String data) { + assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); + } + + @ParameterizedTest + @CsvSource( + value = { + "{}", "[]", "\"hey\"", "20", "false", "null", + }, + delimiter = '|') + void parseInvalidRootObjects(String data) { + assertThatThrownBy(() -> parser.parse(data)).isInstanceOf(OutputParseException.class); + } + + @Test + void parseMetricCalledError() { + Either> result = + parser.parse("{ \"error\": { \"test\": { \"unit\": \"cats\", \"error\": \"Hey\" } } }"); + + assertThat(result.getRight()).isPresent(); + Benchmark benchmark = result.getRight().get().get(0); + assertThat(benchmark.getName()).isEqualTo("error"); + + assertThat(benchmark.getMetrics()).isNotEmpty(); + Metric metric = benchmark.getMetrics().get(0); + assertThat(metric.getName()).isEqualTo("test"); + assertThat(metric.getError()).isPresent(); + assertThat(metric.getError().get()).isEqualTo("Hey"); + } + + @Test + void parseInvalidInterpretationInErrorMessage() { + assertThatThrownBy( + () -> + parser.parse( + "{ \"test\": { \"metric\": { \"error\": \"Yes\", \"interpretation\": \"WHO KNOWS\" } } }")) + .isInstanceOf(OutputParseException.class) + .hasMessageContaining("WHO KNOWS"); + } + + @Test + void parseInvalidUnitInErrorMessage() { + assertThatThrownBy( + () -> + parser.parse("{ \"test\": { \"metric\": { \"error\": \"Yes\", \"unit\": 42 } } }")) + .isInstanceOf(OutputParseException.class) + .hasMessageContaining("42"); + } + + @Test + void parsedMixOldNew() { + // Old values, new interpretation + assertThat( + parser.parse( + "{ \"test\": { \"metric\": { \"values\": [1,2], \"unit\": \"unit\"," + + " \"interpretation\": \"NEUTRAL\" } } }")) + .isEqualTo( + Either.ofRight( + List.of( + new Benchmark( + "test", + List.of( + new Metric( + "metric", + null, + "unit", + Interpretation.NEUTRAL, + List.of(1.0, 2.0))))))); + + // new results, old resultInterpretation + assertThat( + parser.parse( + "{ \"test\": { \"metric\": { \"results\": [1,2], \"unit\": \"unit\"," + + " \"resultInterpretation\": \"NEUTRAL\" } } }")) + .isEqualTo( + Either.ofRight( + List.of( + new Benchmark( + "test", + List.of( + new Metric( + "metric", + null, + "unit", + Interpretation.NEUTRAL, + List.of(1.0, 2.0))))))); + } } diff --git a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedRowsTest.java b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedRowsTest.java index d6972b36e..fb34d9786 100644 --- a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedRowsTest.java +++ b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedRowsTest.java @@ -7,62 +7,56 @@ class NamedRowsTest { - private NamedRows rows; - - @BeforeEach - void setUp() { - rows = new NamedRows(); - } - - @Test - void alignsRowsByLongestName() { - rows.add("hello", "world"); - rows.add("goodbye", "and good night"); - - String expected = "" - + "hello: world\n" - + "goodbye: and good night\n"; - - assertThat(rows.format()).isEqualTo(expected); - - rows.add("a semi-long name", "short text"); - - expected = "" - + "hello: world\n" - + "goodbye: and good night\n" - + "a semi-long name: short text\n"; - - assertThat(rows.format()).isEqualTo(expected); - } - - @Test - void multipleRowsWithTheSameName() { - rows.add("hello", "world"); - rows.add("hello", "there"); - rows.add("bye", "world"); - rows.add("hello", "australia"); - - String expected = "" - + "hello: world\n" - + "hello: there\n" - + "bye: world\n" - + "hello: australia\n"; - - assertThat(rows.format()).isEqualTo(expected); - } - - @Test - void escapingStringsAndArrays() { - rows.addEscaped("String", "foo\bar\n"); - rows.addEscapedArray("Array", new String[]{ - "\"hello\" you there\n\t\n", - "HEY! \n\"" - }); - - String expected = "" - + "String: \"foo\\bar\\n\"\n" - + "Array: [\"\\\"hello\\\" you there\\n\\t\\n\", \"HEY! \\n\\\"\"]\n"; - - assertThat(rows.format()).isEqualTo(expected); - } + private NamedRows rows; + + @BeforeEach + void setUp() { + rows = new NamedRows(); + } + + @Test + void alignsRowsByLongestName() { + rows.add("hello", "world"); + rows.add("goodbye", "and good night"); + + String expected = "" + "hello: world\n" + "goodbye: and good night\n"; + + assertThat(rows.format()).isEqualTo(expected); + + rows.add("a semi-long name", "short text"); + + expected = + "" + + "hello: world\n" + + "goodbye: and good night\n" + + "a semi-long name: short text\n"; + + assertThat(rows.format()).isEqualTo(expected); + } + + @Test + void multipleRowsWithTheSameName() { + rows.add("hello", "world"); + rows.add("hello", "there"); + rows.add("bye", "world"); + rows.add("hello", "australia"); + + String expected = + "" + "hello: world\n" + "hello: there\n" + "bye: world\n" + "hello: australia\n"; + + assertThat(rows.format()).isEqualTo(expected); + } + + @Test + void escapingStringsAndArrays() { + rows.addEscaped("String", "foo\bar\n"); + rows.addEscapedArray("Array", new String[] {"\"hello\" you there\n\t\n", "HEY! \n\""}); + + String expected = + "" + + "String: \"foo\\bar\\n\"\n" + + "Array: [\"\\\"hello\\\" you there\\n\\t\\n\", \"HEY! \\n\\\"\"]\n"; + + assertThat(rows.format()).isEqualTo(expected); + } } diff --git a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedSectionsTest.java b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedSectionsTest.java index 154a8d66d..75164f61e 100644 --- a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedSectionsTest.java +++ b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/formatting/NamedSectionsTest.java @@ -7,128 +7,129 @@ class NamedSectionsTest { - private NamedSections sections; - - @BeforeEach - void setUp() { - sections = new NamedSections(); - } - - @Test - void sectionNamesOfDifferentLength() { - sections.addSection("hello", "world"); - sections.addSection("northrop", "grumman"); - - String expected = "" - + "###########\n" - + "## hello ##\n" - + "###########\n" - + "world\n" - + "\n" - + "##############\n" - + "## northrop ##\n" - + "##############\n" - + "grumman\n"; - - assertThat(sections.format()).isEqualTo(expected); - } - - @Test - void rowModificationsAfterAddingRow() { - NamedRows rows = new NamedRows(); - rows.add("hello", "world"); - sections.addSection("rows", rows); - sections.addSection("not rows", "believe me"); - - String expected = "" - + "##########\n" - + "## rows ##\n" - + "##########\n" - + "hello: world\n" - + "\n" - + "##############\n" - + "## not rows ##\n" - + "##############\n" - + "believe me\n"; - - assertThat(sections.format()).isEqualTo(expected); - - rows.add("keep", "turning"); - - expected = "" - + "##########\n" - + "## rows ##\n" - + "##########\n" - + "hello: world\n" - + "keep: turning\n" - + "\n" - + "##############\n" - + "## not rows ##\n" - + "##############\n" - + "believe me\n"; - - assertThat(sections.format()).isEqualTo(expected); - } - - @Test - void addToFront() { - sections.addSection("hello", "world"); - - String expected = "" - + "###########\n" - + "## hello ##\n" - + "###########\n" - + "world\n"; - - assertThat(sections.format()).isEqualTo(expected); - - sections.addSectionInFront("front!", "I am priority"); - NamedRows rows = new NamedRows(); - rows.add("hello", "world"); - sections.addSectionInFront("frontier", rows); - - expected = "" - + "##############\n" - + "## frontier ##\n" - + "##############\n" - + "hello: world\n" - + "\n" - + "############\n" - + "## front! ##\n" - + "############\n" - + "I am priority\n" - + "\n" - + expected; - - assertThat(sections.format()).isEqualTo(expected); - } - - @Test - void automaticallyAddNewlineWhenNecessary() { - sections.addSection("with newline", "see?\n"); - sections.addSection("without newline", "see?"); - sections.addSection("last section", - "to test if sections are\nalways separated\nby exactly two\nnewlines\n"); - - String expected = "" - + "##################\n" - + "## with newline ##\n" - + "##################\n" - + "see?\n" - + "\n" - + "#####################\n" - + "## without newline ##\n" - + "#####################\n" - + "see?\n" - + "\n" - + "##################\n" - + "## last section ##\n" - + "##################\n" - + "to test if sections are\n" - + "always separated\n" - + "by exactly two\n" - + "newlines\n"; - - assertThat(sections.format()).isEqualTo(expected); - } + private NamedSections sections; + + @BeforeEach + void setUp() { + sections = new NamedSections(); + } + + @Test + void sectionNamesOfDifferentLength() { + sections.addSection("hello", "world"); + sections.addSection("northrop", "grumman"); + + String expected = + "" + + "###########\n" + + "## hello ##\n" + + "###########\n" + + "world\n" + + "\n" + + "##############\n" + + "## northrop ##\n" + + "##############\n" + + "grumman\n"; + + assertThat(sections.format()).isEqualTo(expected); + } + + @Test + void rowModificationsAfterAddingRow() { + NamedRows rows = new NamedRows(); + rows.add("hello", "world"); + sections.addSection("rows", rows); + sections.addSection("not rows", "believe me"); + + String expected = + "" + + "##########\n" + + "## rows ##\n" + + "##########\n" + + "hello: world\n" + + "\n" + + "##############\n" + + "## not rows ##\n" + + "##############\n" + + "believe me\n"; + + assertThat(sections.format()).isEqualTo(expected); + + rows.add("keep", "turning"); + + expected = + "" + + "##########\n" + + "## rows ##\n" + + "##########\n" + + "hello: world\n" + + "keep: turning\n" + + "\n" + + "##############\n" + + "## not rows ##\n" + + "##############\n" + + "believe me\n"; + + assertThat(sections.format()).isEqualTo(expected); + } + + @Test + void addToFront() { + sections.addSection("hello", "world"); + + String expected = "" + "###########\n" + "## hello ##\n" + "###########\n" + "world\n"; + + assertThat(sections.format()).isEqualTo(expected); + + sections.addSectionInFront("front!", "I am priority"); + NamedRows rows = new NamedRows(); + rows.add("hello", "world"); + sections.addSectionInFront("frontier", rows); + + expected = + "" + + "##############\n" + + "## frontier ##\n" + + "##############\n" + + "hello: world\n" + + "\n" + + "############\n" + + "## front! ##\n" + + "############\n" + + "I am priority\n" + + "\n" + + expected; + + assertThat(sections.format()).isEqualTo(expected); + } + + @Test + void automaticallyAddNewlineWhenNecessary() { + sections.addSection("with newline", "see?\n"); + sections.addSection("without newline", "see?"); + sections.addSection( + "last section", "to test if sections are\nalways separated\nby exactly two\nnewlines\n"); + + String expected = + "" + + "##################\n" + + "## with newline ##\n" + + "##################\n" + + "see?\n" + + "\n" + + "#####################\n" + + "## without newline ##\n" + + "#####################\n" + + "see?\n" + + "\n" + + "##################\n" + + "## last section ##\n" + + "##################\n" + + "to test if sections are\n" + + "always separated\n" + + "by exactly two\n" + + "newlines\n"; + + assertThat(sections.format()).isEqualTo(expected); + } } diff --git a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDirTest.java b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDirTest.java index 07eb3203d..2e64fefa2 100644 --- a/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDirTest.java +++ b/backend/runner/src/test/java/de/aaaaaaah/velcom/runner/tmpdirs/BenchRepoDirTest.java @@ -11,65 +11,62 @@ class BenchRepoDirTest { - @TempDir - Path tempFolder; + @TempDir Path tempFolder; - private Path benchRepoDir; - private BenchRepoDir dir; + private Path benchRepoDir; + private BenchRepoDir dir; - @BeforeEach - void setUp() throws IOException { - benchRepoDir = tempFolder.resolve("benchrepo"); - dir = new BenchRepoDir(benchRepoDir); - } + @BeforeEach + void setUp() throws IOException { + benchRepoDir = tempFolder.resolve("benchrepo"); + dir = new BenchRepoDir(benchRepoDir); + } - @Test - void readsNoHash() { - assertThat(dir.getHash()).isEmpty(); - } + @Test + void readsNoHash() { + assertThat(dir.getHash()).isEmpty(); + } - @Test - void writesCorrectHash() throws IOException { - String hash = "12345"; - dir.setHash(hash); + @Test + void writesCorrectHash() throws IOException { + String hash = "12345"; + dir.setHash(hash); - assertThat(dir.getHash()).isPresent().contains(hash); - } + assertThat(dir.getHash()).isPresent().contains(hash); + } - @Test - void readsCorrectHash() throws IOException { - String hash = "12345"; - dir.setHash(hash); + @Test + void readsCorrectHash() throws IOException { + String hash = "12345"; + dir.setHash(hash); - assertThat(new BenchRepoDir(benchRepoDir).getHash()).isPresent().contains(hash); - } + assertThat(new BenchRepoDir(benchRepoDir).getHash()).isPresent().contains(hash); + } - @Test - void clearResetsHash() throws IOException { - String hash = "12345"; - dir.setHash(hash); + @Test + void clearResetsHash() throws IOException { + String hash = "12345"; + dir.setHash(hash); - dir.clear(); + dir.clear(); - assertThat(dir.getHash()).isEmpty(); - } + assertThat(dir.getHash()).isEmpty(); + } - @Test - void clearDeletesRepo() throws IOException { - Files.createDirectory(benchRepoDir); + @Test + void clearDeletesRepo() throws IOException { + Files.createDirectory(benchRepoDir); - Files.writeString(benchRepoDir.resolve("file"), "Hello world"); - Files.createDirectory(benchRepoDir.resolve("directory")); - Files.createDirectory(benchRepoDir.resolve("non-empty-directory")); - Files.writeString( - benchRepoDir.resolve("non-empty-directory").resolve("test.tx"), - "Hello world" - ); + Files.writeString(benchRepoDir.resolve("file"), "Hello world"); + Files.createDirectory(benchRepoDir.resolve("directory")); + Files.createDirectory(benchRepoDir.resolve("non-empty-directory")); + Files.writeString( + benchRepoDir.resolve("non-empty-directory").resolve("test.tx"), "Hello world"); - assertThat(benchRepoDir).isNotEmptyDirectory(); + assertThat(benchRepoDir).isNotEmptyDirectory(); - dir.clear(); + dir.clear(); - assertThat(benchRepoDir).doesNotExist(); - } + assertThat(benchRepoDir).doesNotExist(); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/GitProperties.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/GitProperties.java index 97c5d4ae1..5a9f0e7d7 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/GitProperties.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/GitProperties.java @@ -6,35 +6,35 @@ /** * Provides some build information such as the build time, commit hash or version. * - *

When changing this, also update the sections marked with the comment "Current commit hash + *

When changing this, also update the sections marked with the comment "Current commit hash * available to jars" in backend/pom.xml and runner/pom.xml. */ public class GitProperties { - private static final Properties PROPERTIES; + private static final Properties PROPERTIES; - static { - PROPERTIES = new Properties(); - try { - PROPERTIES.load(GitProperties.class.getResourceAsStream("/git.properties")); - } catch (IOException e) { - throw new RuntimeException("Could not initialize git properties", e); - } - } + static { + PROPERTIES = new Properties(); + try { + PROPERTIES.load(GitProperties.class.getResourceAsStream("/git.properties")); + } catch (IOException e) { + throw new RuntimeException("Could not initialize git properties", e); + } + } - public static String getBuildTime() { - return PROPERTIES.getProperty("git.build.time"); - } + public static String getBuildTime() { + return PROPERTIES.getProperty("git.build.time"); + } - public static String getVersion() { - return PROPERTIES.getProperty("git.build.version"); - } + public static String getVersion() { + return PROPERTIES.getProperty("git.build.version"); + } - public static String getHash() { - return PROPERTIES.getProperty("git.commit.id.full"); - } + public static String getHash() { + return PROPERTIES.getProperty("git.commit.id.full"); + } - public static String getHashAbbrev() { - return PROPERTIES.getProperty("git.commit.id.abbrev"); - } + public static String getHashAbbrev() { + return PROPERTIES.getProperty("git.commit.id.abbrev"); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandler.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandler.java index e4fc297c4..096e1e2b3 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandler.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandler.java @@ -6,101 +6,92 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * Sends and receives heartbeats, calling a timeout handler if appropriate. - */ +/** Sends and receives heartbeats, calling a timeout handler if appropriate. */ public class HeartbeatHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(HeartbeatHandler.class); + private static final Logger LOGGER = LoggerFactory.getLogger(HeartbeatHandler.class); - private static final int DEFAULT_TIMEOUT_SECONDS = 200; + private static final int DEFAULT_TIMEOUT_SECONDS = 200; - private final AtomicReference lastTime; - private volatile boolean run; + private final AtomicReference lastTime; + private volatile boolean run; - /** - * Creates a new heartbeat handler for the given socket. - * - *

- * Uses the default timeout of currently 200 seconds. - * - * @param socketConnection the socket to monitor - */ - public HeartbeatHandler(HeartbeatWebsocket socketConnection) { - this(socketConnection, DEFAULT_TIMEOUT_SECONDS * 1000); - } + /** + * Creates a new heartbeat handler for the given socket. + * + *

Uses the default timeout of currently 200 seconds. + * + * @param socketConnection the socket to monitor + */ + public HeartbeatHandler(HeartbeatWebsocket socketConnection) { + this(socketConnection, DEFAULT_TIMEOUT_SECONDS * 1000); + } - /** - * Creates a new heartbeat handler for the given socket. - * - * @param socketConnection the socket to monitor - * @param timeoutMillis the duration in millis after which the connection is dropped - */ - public HeartbeatHandler(HeartbeatWebsocket socketConnection, int timeoutMillis) { - this.lastTime = new AtomicReference<>(Instant.now()); - this.run = true; + /** + * Creates a new heartbeat handler for the given socket. + * + * @param socketConnection the socket to monitor + * @param timeoutMillis the duration in millis after which the connection is dropped + */ + public HeartbeatHandler(HeartbeatWebsocket socketConnection, int timeoutMillis) { + this.lastTime = new AtomicReference<>(Instant.now()); + this.run = true; - Thread thread = new Thread(() -> { - while (run) { - try { - //noinspection BusyWait - Thread.sleep(timeoutMillis / 2); - } catch (InterruptedException ignored) { - } - if (!run) { - break; - } - LOGGER.debug("Sending ping!"); + Thread thread = + new Thread( + () -> { + while (run) { + try { + //noinspection BusyWait + Thread.sleep(timeoutMillis / 2); + } catch (InterruptedException ignored) { + } + if (!run) { + break; + } + LOGGER.debug("Sending ping!"); - // If the ping was successful, it was *SENT* successfully. - // This does not mean we got an answer. - // If sending the ping failed already, that is a deeper problem the socket should - // habdle - and we do not try to! - if (!socketConnection.sendPing()) { - LOGGER.warn("Ping to runner/server failed!"); - continue; - } - long millisSinceLastPing = Duration.between(lastTime.get(), Instant.now()) - .toMillis(); - if (millisSinceLastPing > timeoutMillis) { - socketConnection.onTimeoutDetected(); - } - } - }); - thread.setDaemon(true); - thread.start(); - } + // If the ping was successful, it was *SENT* successfully. + // This does not mean we got an answer. + // If sending the ping failed already, that is a deeper problem the socket should + // habdle - and we do not try to! + if (!socketConnection.sendPing()) { + LOGGER.warn("Ping to runner/server failed!"); + continue; + } + long millisSinceLastPing = + Duration.between(lastTime.get(), Instant.now()).toMillis(); + if (millisSinceLastPing > timeoutMillis) { + socketConnection.onTimeoutDetected(); + } + } + }); + thread.setDaemon(true); + thread.start(); + } - /** - * Should be called when a pong was received. - */ - public void onPong() { - LOGGER.debug("Got pong!"); - lastTime.set(Instant.now()); - } + /** Should be called when a pong was received. */ + public void onPong() { + LOGGER.debug("Got pong!"); + lastTime.set(Instant.now()); + } - /** - * Shuts down this manager. - */ - public void shutdown() { - run = false; - } + /** Shuts down this manager. */ + public void shutdown() { + run = false; + } - /** - * An abstraction for a websocket that is usable for this manager. - */ - public interface HeartbeatWebsocket { + /** An abstraction for a websocket that is usable for this manager. */ + public interface HeartbeatWebsocket { - /** - * Called when the manager detects a timeout. - */ - void onTimeoutDetected(); + /** Called when the manager detects a timeout. */ + void onTimeoutDetected(); - /** - * Sends a ping message. Calls {@link #onPong()} if the resulting pong is detected. - * - * @return true if the ping was sent successfully - */ - boolean sendPing(); - } + /** + * Sends a ping message. Calls {@link #onPong()} if the resulting pong is detected. + * + * @return true if the ping was sent successfully + */ + boolean sendPing(); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerConnectionHeader.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerConnectionHeader.java index d95adf905..b6f954ae2 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerConnectionHeader.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerConnectionHeader.java @@ -1,23 +1,21 @@ package de.aaaaaaah.velcom.shared.protocol; -/** - * A collection of shared header names needed during the runner-backend connection. - */ +/** A collection of shared header names needed during the runner-backend connection. */ public enum RunnerConnectionHeader { - CONNECT_RUNNER_TOKEN("Runner-Token"), - CONNECT_RUNNER_NAME("Runner-Name"), - DISCONNECT_DENY_REASON("Runner-Deny"); + CONNECT_RUNNER_TOKEN("Runner-Token"), + CONNECT_RUNNER_NAME("Runner-Name"), + DISCONNECT_DENY_REASON("Runner-Deny"); - private final String headerName; + private final String headerName; - RunnerConnectionHeader(String headerName) { - this.headerName = headerName; - } + RunnerConnectionHeader(String headerName) { + this.headerName = headerName; + } - /** - * @return the name of the header - */ - public String getName() { - return headerName; - } + /** + * @return the name of the header + */ + public String getName() { + return headerName; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerDenyReason.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerDenyReason.java index 7560a1f67..ead888731 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerDenyReason.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/RunnerDenyReason.java @@ -1,40 +1,38 @@ package de.aaaaaaah.velcom.shared.protocol; -/** - * The possible reasons when the runner's connection attempt was denied. - */ +/** The possible reasons when the runner's connection attempt was denied. */ public enum RunnerDenyReason { - TOKEN_INVALID(401, "your token is invalid", "TOKEN"), - NAME_ALREADY_USED(403, "your name is already taken", "NAME"); + TOKEN_INVALID(401, "your token is invalid", "TOKEN"), + NAME_ALREADY_USED(403, "your name is already taken", "NAME"); - private final int code; - private final String message; - private final String headerValue; + private final int code; + private final String message; + private final String headerValue; - RunnerDenyReason(int code, String message, String headerValue) { - this.code = code; - this.message = message; - this.headerValue = headerValue; - } + RunnerDenyReason(int code, String message, String headerValue) { + this.code = code; + this.message = message; + this.headerValue = headerValue; + } - /** - * @return the value the "Runner-Deny" header will contain - */ - public String getHeaderValue() { - return headerValue; - } + /** + * @return the value the "Runner-Deny" header will contain + */ + public String getHeaderValue() { + return headerValue; + } - /** - * @return the http response code associated with this event - */ - public int getCode() { - return code; - } + /** + * @return the http response code associated with this event + */ + public int getCode() { + return code; + } - /** - * @return the reason the runner's connection attempt was denied as a human readable message - */ - public String getMessage() { - return message; - } + /** + * @return the reason the runner's connection attempt was denied as a human readable message + */ + public String getMessage() { + return message; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/StatusCode.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/StatusCode.java index 073b7ebfc..2fe0bce92 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/StatusCode.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/StatusCode.java @@ -8,61 +8,59 @@ * runner. */ public enum StatusCode { - NORMAL_CLOSURE(1000, ""), - INTERNAL_ERROR(4000, "internal error"), - ILLEGAL_BEHAVIOUR(4001, "illegal behaviour has occurred"), - ILLEGAL_PACKET(4002, "invalid packet or packet of illegal type"), - ILLEGAL_BINARY_PACKET(4003, "binary packet when none was allowed"), - PING_TIMEOUT(4004, "ping timed out"), - COMMAND_TIMEOUT(4005, "command reply timed out"), - NO_RESULT(4006, "tried to operate on absent result"), - TRANSFER_FAILED(4007, "transferring (binary) data failed"); + NORMAL_CLOSURE(1000, ""), + INTERNAL_ERROR(4000, "internal error"), + ILLEGAL_BEHAVIOUR(4001, "illegal behaviour has occurred"), + ILLEGAL_PACKET(4002, "invalid packet or packet of illegal type"), + ILLEGAL_BINARY_PACKET(4003, "binary packet when none was allowed"), + PING_TIMEOUT(4004, "ping timed out"), + COMMAND_TIMEOUT(4005, "command reply timed out"), + NO_RESULT(4006, "tried to operate on absent result"), + TRANSFER_FAILED(4007, "transferring (binary) data failed"); - private final int code; - private final String description; + private final int code; + private final String description; - StatusCode(int code, String description) { - this.code = code; - this.description = description; - } + StatusCode(int code, String description) { + this.code = code; + this.description = description; + } - /** - * Try to interpret a websocket status code as {@link StatusCode}. - * - * @param code the websocket status code - * @return the {@link StatusCode}, if successful - */ - public static Optional fromCode(int code) { - return Arrays.stream(values()) - .filter(statusCode -> statusCode.getCode() == code) - .findFirst(); - } + /** + * Try to interpret a websocket status code as {@link StatusCode}. + * + * @param code the websocket status code + * @return the {@link StatusCode}, if successful + */ + public static Optional fromCode(int code) { + return Arrays.stream(values()).filter(statusCode -> statusCode.getCode() == code).findFirst(); + } - public int getCode() { - return code; - } + public int getCode() { + return code; + } - /** - * A human-readable description of the status code. Don't use this as the websocket close reason! - * Instead, use {@link #getDescriptionAsReason()}. - * - * @return the status code description - */ - public String getDescription() { - return description; - } + /** + * A human-readable description of the status code. Don't use this as the websocket close reason! + * Instead, use {@link #getDescriptionAsReason()}. + * + * @return the status code description + */ + public String getDescription() { + return description; + } - /** - * A method for safely using the status code description as websocket close reason. - * - * @return the description returned by {@link #getDescription()} or an empty string if the - * description is too long - */ - public String getDescriptionAsReason() { - if (description.getBytes().length > 123) { - return ""; - } else { - return description; - } - } + /** + * A method for safely using the status code description as websocket close reason. + * + * @return the description returned by {@link #getDescription()} or an empty string if the + * description is too long + */ + public String getDescriptionAsReason() { + if (description.getBytes().length > 123) { + return ""; + } else { + return description; + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Result.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Result.java index e39d9b37e..81c4612cb 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Result.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Result.java @@ -7,199 +7,188 @@ import java.util.Optional; import javax.annotation.Nullable; -/** - * The result of a run, in a format that is easy to serialize and deserialize. - */ +/** The result of a run, in a format that is easy to serialize and deserialize. */ public class Result { - @Nullable - private final List benchmarks; - @Nullable - private final String error; - - public Result(@Nullable List benchmarks, @Nullable String error) { - if (benchmarks == null && error == null) { - throw new IllegalArgumentException("benchmarks and error can't both be null"); - } else if (benchmarks != null && error != null) { - throw new IllegalArgumentException("benchmarks and error can't both contain a value"); - } - - this.benchmarks = benchmarks; - this.error = error; - } - - public Optional> getBenchmarks() { - return Optional.ofNullable(benchmarks); - } - - public Optional getError() { - return Optional.ofNullable(error); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Result result = (Result) o; - return Objects.equals(benchmarks, result.benchmarks) && - Objects.equals(error, result.error); - } - - @Override - public int hashCode() { - return Objects.hash(benchmarks, error); - } - - /** - * Possible result interpretations for a single metric. - */ - public enum Interpretation { - LESS_IS_BETTER, - MORE_IS_BETTER, - NEUTRAL - } - - /** - * A benchmark contains multiple metrics. - */ - public static class Benchmark { - - private final String name; - private final List metrics; - - @JsonCreator - public Benchmark( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) List metrics - ) { - this.name = name; - this.metrics = metrics; - } - - public String getName() { - return name; - } - - public List getMetrics() { - return metrics; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Benchmark benchmark = (Benchmark) o; - return name.equals(benchmark.name) && - metrics.equals(benchmark.metrics); - } - - @Override - public int hashCode() { - return Objects.hash(name, metrics); - } - - @Override - public String toString() { - return "Benchmark{" + - "name='" + name + '\'' + - ", metrics=" + metrics + - '}'; - } - } - - /** - * A metric is either successful or failed, If it is successful, it contains the measured values - * and some extra information. If it is failed, it contains only an error message. - */ - public static class Metric { - - private final String name; - @Nullable - private final String error; - @Nullable - private final String unit; - @Nullable - private final Interpretation interpretation; - @Nullable - private final List values; - - @JsonCreator - public Metric( - @JsonProperty(required = true) String name, - @Nullable String error, - @Nullable String unit, - @Nullable Interpretation interpretation, - @Nullable List values - ) { - if (error == null && values == null) { - throw new IllegalArgumentException("if error is null, values must not be null"); - } - - this.name = name; - this.error = error; - this.unit = unit; - this.interpretation = interpretation; - this.values = values; - } - - public String getName() { - return name; - } - - public Optional getError() { - return Optional.ofNullable(error); - } - - public Optional getUnit() { - return Optional.ofNullable(unit); - } - - public Optional getInterpretation() { - return Optional.ofNullable(interpretation); - } - - public Optional> getValues() { - return Optional.ofNullable(values); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Metric metric = (Metric) o; - return Objects.equals(error, metric.error) && - Objects.equals(name, metric.name) && - Objects.equals(unit, metric.unit) && - interpretation == metric.interpretation && - Objects.equals(values, metric.values); - } - - @Override - public int hashCode() { - return Objects.hash(error, name, unit, interpretation, values); - } - - @Override - public String toString() { - return "Metric{" + - "name='" + name + '\'' + - ", error='" + error + '\'' + - ", unit='" + unit + '\'' + - ", interpretation=" + interpretation + - ", values=" + values + - '}'; - } - } + @Nullable private final List benchmarks; + @Nullable private final String error; + + public Result(@Nullable List benchmarks, @Nullable String error) { + if (benchmarks == null && error == null) { + throw new IllegalArgumentException("benchmarks and error can't both be null"); + } else if (benchmarks != null && error != null) { + throw new IllegalArgumentException("benchmarks and error can't both contain a value"); + } + + this.benchmarks = benchmarks; + this.error = error; + } + + public Optional> getBenchmarks() { + return Optional.ofNullable(benchmarks); + } + + public Optional getError() { + return Optional.ofNullable(error); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Result result = (Result) o; + return Objects.equals(benchmarks, result.benchmarks) && Objects.equals(error, result.error); + } + + @Override + public int hashCode() { + return Objects.hash(benchmarks, error); + } + + /** Possible result interpretations for a single metric. */ + public enum Interpretation { + LESS_IS_BETTER, + MORE_IS_BETTER, + NEUTRAL + } + + /** A benchmark contains multiple metrics. */ + public static class Benchmark { + + private final String name; + private final List metrics; + + @JsonCreator + public Benchmark( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) List metrics) { + this.name = name; + this.metrics = metrics; + } + + public String getName() { + return name; + } + + public List getMetrics() { + return metrics; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Benchmark benchmark = (Benchmark) o; + return name.equals(benchmark.name) && metrics.equals(benchmark.metrics); + } + + @Override + public int hashCode() { + return Objects.hash(name, metrics); + } + + @Override + public String toString() { + return "Benchmark{" + "name='" + name + '\'' + ", metrics=" + metrics + '}'; + } + } + + /** + * A metric is either successful or failed, If it is successful, it contains the measured values + * and some extra information. If it is failed, it contains only an error message. + */ + public static class Metric { + + private final String name; + @Nullable private final String error; + @Nullable private final String unit; + @Nullable private final Interpretation interpretation; + @Nullable private final List values; + + @JsonCreator + public Metric( + @JsonProperty(required = true) String name, + @Nullable String error, + @Nullable String unit, + @Nullable Interpretation interpretation, + @Nullable List values) { + if (error == null && values == null) { + throw new IllegalArgumentException("if error is null, values must not be null"); + } + + this.name = name; + this.error = error; + this.unit = unit; + this.interpretation = interpretation; + this.values = values; + } + + public String getName() { + return name; + } + + public Optional getError() { + return Optional.ofNullable(error); + } + + public Optional getUnit() { + return Optional.ofNullable(unit); + } + + public Optional getInterpretation() { + return Optional.ofNullable(interpretation); + } + + public Optional> getValues() { + return Optional.ofNullable(values); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Metric metric = (Metric) o; + return Objects.equals(error, metric.error) + && Objects.equals(name, metric.name) + && Objects.equals(unit, metric.unit) + && interpretation == metric.interpretation + && Objects.equals(values, metric.values); + } + + @Override + public int hashCode() { + return Objects.hash(error, name, unit, interpretation, values); + } + + @Override + public String toString() { + return "Metric{" + + "name='" + + name + + '\'' + + ", error='" + + error + + '\'' + + ", unit='" + + unit + + '\'' + + ", interpretation=" + + interpretation + + ", values=" + + values + + '}'; + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Serializer.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Serializer.java index 4f634834e..3ed6af709 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Serializer.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Serializer.java @@ -16,71 +16,71 @@ */ public class Serializer { - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - public Serializer() { - objectMapper = new ObjectMapper() - .registerModule(new ParameterNamesModule()) - .registerModule(new Jdk8Module()) - .registerModule(new JavaTimeModule()) - .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - } + public Serializer() { + objectMapper = + new ObjectMapper() + .registerModule(new ParameterNamesModule()) + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + } - /** - * Serialize an object to JSON. - * - * @param value the object to serialize - * @return the JSON representation of that object, if successful - */ - public Optional serialize(Object value) { - try { - return Optional.of(objectMapper.writeValueAsString(value)); - } catch (JsonProcessingException e) { - return Optional.empty(); - } - } + /** + * Serialize an object to JSON. + * + * @param value the object to serialize + * @return the JSON representation of that object, if successful + */ + public Optional serialize(Object value) { + try { + return Optional.of(objectMapper.writeValueAsString(value)); + } catch (JsonProcessingException e) { + return Optional.empty(); + } + } - /** - * Serialize an object to a JSON tree. - * - * @param value the object to serialize - * @return the JSON representation of that object - */ - public JsonNode serializeTree(Object value) { - return objectMapper.valueToTree(value); - } + /** + * Serialize an object to a JSON tree. + * + * @param value the object to serialize + * @return the JSON representation of that object + */ + public JsonNode serializeTree(Object value) { + return objectMapper.valueToTree(value); + } - /** - * Deserialize an object from a JSON string. - * - * @param input the string to read from - * @param type the class of the type to deserialize into - * @param the type to deserialize into - * @return the deserialized object, if successful - */ - public Optional deserialize(String input, Class type) { - try { - return Optional.of(objectMapper.readValue(input, type)); - } catch (JsonProcessingException e) { - return Optional.empty(); - } - } - - /** - * Deserialize an object from a JSON tree. - * - * @param input the input tree to use - * @param type the class of the type to deserialize into - * @param the type to deserialize into - * @return the deserialized object, if successful - */ - public Optional deserialize(JsonNode input, Class type) { - try { - return Optional.of(objectMapper.treeToValue(input, type)); - } catch (JsonProcessingException e) { - return Optional.empty(); - } - } + /** + * Deserialize an object from a JSON string. + * + * @param input the string to read from + * @param type the class of the type to deserialize into + * @param the type to deserialize into + * @return the deserialized object, if successful + */ + public Optional deserialize(String input, Class type) { + try { + return Optional.of(objectMapper.readValue(input, type)); + } catch (JsonProcessingException e) { + return Optional.empty(); + } + } + /** + * Deserialize an object from a JSON tree. + * + * @param input the input tree to use + * @param type the class of the type to deserialize into + * @param the type to deserialize into + * @return the deserialized object, if successful + */ + public Optional deserialize(JsonNode input, Class type) { + try { + return Optional.of(objectMapper.treeToValue(input, type)); + } catch (JsonProcessingException e) { + return Optional.empty(); + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Status.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Status.java index d0bf6c890..fe82e921d 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Status.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/Status.java @@ -1,21 +1,13 @@ package de.aaaaaaah.velcom.shared.protocol.serialization; -/** - * This enum describes a runner's state from the view of a backend it is connected to. - */ +/** This enum describes a runner's state from the view of a backend it is connected to. */ public enum Status { - /** - * The runner is running a benchmark. - */ - RUN, + /** The runner is running a benchmark. */ + RUN, - /** - * The runner is aborting a benchmark. - */ - ABORT, + /** The runner is aborting a benchmark. */ + ABORT, - /** - * The runner is not running a benchmark. - */ - IDLE + /** The runner is not running a benchmark. */ + IDLE } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRun.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRun.java index 1bddd08b0..daaae510d 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRun.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRun.java @@ -9,11 +9,8 @@ */ public class AbortRun implements ClientBound { - @Override - public ClientBoundPacket asPacket(Serializer serializer) { - return new ClientBoundPacket( - ClientBoundPacketType.ABORT_RUN, - serializer.serializeTree(this) - ); - } + @Override + public ClientBoundPacket asPacket(Serializer serializer) { + return new ClientBoundPacket(ClientBoundPacketType.ABORT_RUN, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResult.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResult.java index 9e40436d4..bd6923f85 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResult.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResult.java @@ -7,11 +7,9 @@ */ public class ClearResult implements ClientBound { - @Override - public ClientBoundPacket asPacket(Serializer serializer) { - return new ClientBoundPacket( - ClientBoundPacketType.CLEAR_RESULT, - serializer.serializeTree(this) - ); - } + @Override + public ClientBoundPacket asPacket(Serializer serializer) { + return new ClientBoundPacket( + ClientBoundPacketType.CLEAR_RESULT, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBound.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBound.java index 80162993b..ba23129eb 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBound.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBound.java @@ -2,16 +2,14 @@ import de.aaaaaaah.velcom.shared.protocol.serialization.Serializer; -/** - * Packets that can be converted into a {@link ClientBoundPacket}. - */ +/** Packets that can be converted into a {@link ClientBoundPacket}. */ public interface ClientBound { - /** - * Convert this packet to a {@link ClientBoundPacket}. - * - * @param serializer the JSON serializer instance to use - * @return the {@link ClientBoundPacket} - */ - ClientBoundPacket asPacket(Serializer serializer); + /** + * Convert this packet to a {@link ClientBoundPacket}. + * + * @param serializer the JSON serializer instance to use + * @return the {@link ClientBoundPacket} + */ + ClientBoundPacket asPacket(Serializer serializer); } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacket.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacket.java index 330d36278..98fd8e5b9 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacket.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacket.java @@ -5,46 +5,42 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.Objects; -/** - * A packet that can be sent to the runner by the backend. - */ +/** A packet that can be sent to the runner by the backend. */ public class ClientBoundPacket { - private final ClientBoundPacketType type; - private final JsonNode data; - - @JsonCreator - public ClientBoundPacket( - @JsonProperty(required = true) ClientBoundPacketType type, - @JsonProperty(required = true) JsonNode data - ) { - this.type = type; - this.data = data; - } - - public ClientBoundPacketType getType() { - return type; - } - - public JsonNode getData() { - return data; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ClientBoundPacket that = (ClientBoundPacket) o; - return type == that.type && - data.equals(that.data); - } - - @Override - public int hashCode() { - return Objects.hash(type, data); - } + private final ClientBoundPacketType type; + private final JsonNode data; + + @JsonCreator + public ClientBoundPacket( + @JsonProperty(required = true) ClientBoundPacketType type, + @JsonProperty(required = true) JsonNode data) { + this.type = type; + this.data = data; + } + + public ClientBoundPacketType getType() { + return type; + } + + public JsonNode getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ClientBoundPacket that = (ClientBoundPacket) o; + return type == that.type && data.equals(that.data); + } + + @Override + public int hashCode() { + return Objects.hash(type, data); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketType.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketType.java index 42ff1d993..7a892bc5f 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketType.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketType.java @@ -3,48 +3,46 @@ import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; -/** - * The types of packets that can be sent to the runner by the backend. - */ +/** The types of packets that can be sent to the runner by the backend. */ public enum ClientBoundPacketType { - ABORT_RUN("abort_run", AbortRun.class), - CLEAR_RESULT("clear_result", ClearResult.class), - GET_RESULT("get_result", GetResult.class), - GET_STATUS("get_status", GetStatus.class), - REQUEST_RUN_REPLY("request_run_reply", RequestRunReply.class); - - private final String type; - private final Class dataClass; - - ClientBoundPacketType(String type, Class dataClass) { - this.type = type; - this.dataClass = dataClass; - } - - static Optional ofType(String type) { - for (ClientBoundPacketType value : ClientBoundPacketType.values()) { - if (value.type.equals(type)) { - return Optional.of(value); - } - } - return Optional.empty(); - } - - static Optional ofClass(Class theClass) { - for (ClientBoundPacketType value : ClientBoundPacketType.values()) { - if (value.dataClass.equals(theClass)) { - return Optional.of(value); - } - } - return Optional.empty(); - } - - @JsonValue - public String getType() { - return type; - } - - public Class getDataClass() { - return dataClass; - } + ABORT_RUN("abort_run", AbortRun.class), + CLEAR_RESULT("clear_result", ClearResult.class), + GET_RESULT("get_result", GetResult.class), + GET_STATUS("get_status", GetStatus.class), + REQUEST_RUN_REPLY("request_run_reply", RequestRunReply.class); + + private final String type; + private final Class dataClass; + + ClientBoundPacketType(String type, Class dataClass) { + this.type = type; + this.dataClass = dataClass; + } + + static Optional ofType(String type) { + for (ClientBoundPacketType value : ClientBoundPacketType.values()) { + if (value.type.equals(type)) { + return Optional.of(value); + } + } + return Optional.empty(); + } + + static Optional ofClass(Class theClass) { + for (ClientBoundPacketType value : ClientBoundPacketType.values()) { + if (value.dataClass.equals(theClass)) { + return Optional.of(value); + } + } + return Optional.empty(); + } + + @JsonValue + public String getType() { + return type; + } + + public Class getDataClass() { + return dataClass; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResult.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResult.java index d170f8916..2ffcf158f 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResult.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResult.java @@ -8,11 +8,8 @@ */ public class GetResult implements ClientBound { - @Override - public ClientBoundPacket asPacket(Serializer serializer) { - return new ClientBoundPacket( - ClientBoundPacketType.GET_RESULT, - serializer.serializeTree(this) - ); - } + @Override + public ClientBoundPacket asPacket(Serializer serializer) { + return new ClientBoundPacket(ClientBoundPacketType.GET_RESULT, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatus.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatus.java index dc0b84786..ac9524609 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatus.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatus.java @@ -2,16 +2,11 @@ import de.aaaaaaah.velcom.shared.protocol.serialization.Serializer; -/** - * A command requesting the runner's current status. - */ +/** A command requesting the runner's current status. */ public class GetStatus implements ClientBound { - @Override - public ClientBoundPacket asPacket(Serializer serializer) { - return new ClientBoundPacket( - ClientBoundPacketType.GET_STATUS, - serializer.serializeTree(this) - ); - } + @Override + public ClientBoundPacket asPacket(Serializer serializer) { + return new ClientBoundPacket(ClientBoundPacketType.GET_STATUS, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReply.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReply.java index 6d75d27e8..549000639 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReply.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReply.java @@ -14,85 +14,80 @@ */ public class RequestRunReply implements ClientBound { - private final boolean bench; - @Nullable - private final String benchHash; - private final boolean run; - @Nullable - private final UUID runId; + private final boolean bench; + @Nullable private final String benchHash; + private final boolean run; + @Nullable private final UUID runId; - @JsonCreator - public RequestRunReply( - @JsonProperty(required = true) boolean bench, - @Nullable String benchHash, - @JsonProperty(required = true) boolean run, - @Nullable UUID runId - ) { - if (bench && benchHash == null) { - throw new IllegalArgumentException("if bench is true, bench_hash must not be null"); - } else if (!bench && benchHash != null) { - throw new IllegalArgumentException("if bench is false, bench_hash must be null"); - } else if (run && runId == null) { - throw new IllegalArgumentException("if run is true, run_id must not be null"); - } else if (!run && runId != null) { - throw new IllegalArgumentException("if run is false, run_id must be null"); - } + @JsonCreator + public RequestRunReply( + @JsonProperty(required = true) boolean bench, + @Nullable String benchHash, + @JsonProperty(required = true) boolean run, + @Nullable UUID runId) { + if (bench && benchHash == null) { + throw new IllegalArgumentException("if bench is true, bench_hash must not be null"); + } else if (!bench && benchHash != null) { + throw new IllegalArgumentException("if bench is false, bench_hash must be null"); + } else if (run && runId == null) { + throw new IllegalArgumentException("if run is true, run_id must not be null"); + } else if (!run && runId != null) { + throw new IllegalArgumentException("if run is false, run_id must be null"); + } - this.bench = bench; - this.benchHash = benchHash; - this.run = run; - this.runId = runId; - } + this.bench = bench; + this.benchHash = benchHash; + this.run = run; + this.runId = runId; + } - /** - * @return whether the backend will send a new benchmark repo - */ - @JsonProperty("bench") - public boolean hasBench() { - return bench; - } + /** + * @return whether the backend will send a new benchmark repo + */ + @JsonProperty("bench") + public boolean hasBench() { + return bench; + } - public Optional getBenchHash() { - return Optional.ofNullable(benchHash); - } + public Optional getBenchHash() { + return Optional.ofNullable(benchHash); + } - /** - * @return whether the backend will send a new repo for the runner to benchmark - */ - @JsonProperty("run") - public boolean hasRun() { - return run; - } + /** + * @return whether the backend will send a new repo for the runner to benchmark + */ + @JsonProperty("run") + public boolean hasRun() { + return run; + } - public Optional getRunId() { - return Optional.ofNullable(runId); - } + public Optional getRunId() { + return Optional.ofNullable(runId); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RequestRunReply that = (RequestRunReply) o; - return bench == that.bench && - run == that.run && - Objects.equals(benchHash, that.benchHash) && - Objects.equals(runId, that.runId); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RequestRunReply that = (RequestRunReply) o; + return bench == that.bench + && run == that.run + && Objects.equals(benchHash, that.benchHash) + && Objects.equals(runId, that.runId); + } - @Override - public int hashCode() { - return Objects.hash(bench, benchHash, run, runId); - } + @Override + public int hashCode() { + return Objects.hash(bench, benchHash, run, runId); + } - @Override - public ClientBoundPacket asPacket(Serializer serializer) { - return new ClientBoundPacket( - ClientBoundPacketType.REQUEST_RUN_REPLY, - serializer.serializeTree(this) - ); - } + @Override + public ClientBoundPacket asPacket(Serializer serializer) { + return new ClientBoundPacket( + ClientBoundPacketType.REQUEST_RUN_REPLY, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReply.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReply.java index f584dca3a..617ad111f 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReply.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReply.java @@ -8,11 +8,9 @@ */ public class AbortRunReply implements ServerBound { - @Override - public ServerBoundPacket asPacket(Serializer serializer) { - return new ServerBoundPacket( - ServerBoundPacketType.ABORT_RUN_REPLY, - serializer.serializeTree(this) - ); - } + @Override + public ServerBoundPacket asPacket(Serializer serializer) { + return new ServerBoundPacket( + ServerBoundPacketType.ABORT_RUN_REPLY, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReply.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReply.java index 0a0ef7c53..9e1def36f 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReply.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReply.java @@ -2,16 +2,12 @@ import de.aaaaaaah.velcom.shared.protocol.serialization.Serializer; -/** - * Reply indicating the current result was cleared. - */ +/** Reply indicating the current result was cleared. */ public class ClearResultReply implements ServerBound { - @Override - public ServerBoundPacket asPacket(Serializer serializer) { - return new ServerBoundPacket( - ServerBoundPacketType.CLEAR_RESULT_REPLY, - serializer.serializeTree(this) - ); - } + @Override + public ServerBoundPacket asPacket(Serializer serializer) { + return new ServerBoundPacket( + ServerBoundPacketType.CLEAR_RESULT_REPLY, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReply.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReply.java index a1c18657c..b4c32cb9e 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReply.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReply.java @@ -17,88 +17,82 @@ */ public class GetResultReply implements ServerBound { - private final UUID runId; - private final boolean success; - @Nullable - private final Result result; - @Nullable - private final String error; - private final Instant startTime; - private final Instant stopTime; + private final UUID runId; + private final boolean success; + @Nullable private final Result result; + @Nullable private final String error; + private final Instant startTime; + private final Instant stopTime; - @JsonCreator - public GetResultReply( - @JsonProperty(required = true) UUID runId, - @JsonProperty(required = true) boolean success, - @Nullable Result result, - @Nullable String error, - @JsonProperty(required = true) Instant startTime, - @JsonProperty(required = true) Instant stopTime) { - if (success && (result == null || error != null)) { - throw new IllegalArgumentException( - "if successful, there must be a result and no error"); - } else if (!success && (result != null || error == null)) { - throw new IllegalArgumentException( - "if not successful, there must be an error and no result"); - } + @JsonCreator + public GetResultReply( + @JsonProperty(required = true) UUID runId, + @JsonProperty(required = true) boolean success, + @Nullable Result result, + @Nullable String error, + @JsonProperty(required = true) Instant startTime, + @JsonProperty(required = true) Instant stopTime) { + if (success && (result == null || error != null)) { + throw new IllegalArgumentException("if successful, there must be a result and no error"); + } else if (!success && (result != null || error == null)) { + throw new IllegalArgumentException("if not successful, there must be an error and no result"); + } - this.startTime = startTime; - this.stopTime = stopTime; - this.runId = runId; - this.success = success; - this.result = result; - this.error = error; - } + this.startTime = startTime; + this.stopTime = stopTime; + this.runId = runId; + this.success = success; + this.result = result; + this.error = error; + } - public UUID getRunId() { - return runId; - } + public UUID getRunId() { + return runId; + } - public boolean isSuccess() { - return success; - } + public boolean isSuccess() { + return success; + } - public Optional getResult() { - return Optional.ofNullable(result); - } + public Optional getResult() { + return Optional.ofNullable(result); + } - public Optional getError() { - return Optional.ofNullable(error); - } + public Optional getError() { + return Optional.ofNullable(error); + } - public Instant getStartTime() { - return startTime; - } + public Instant getStartTime() { + return startTime; + } - public Instant getStopTime() { - return stopTime; - } + public Instant getStopTime() { + return stopTime; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GetResultReply that = (GetResultReply) o; - return success == that.success && - runId.equals(that.runId) && - Objects.equals(result, that.result) && - Objects.equals(error, that.error); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetResultReply that = (GetResultReply) o; + return success == that.success + && runId.equals(that.runId) + && Objects.equals(result, that.result) + && Objects.equals(error, that.error); + } - @Override - public int hashCode() { - return Objects.hash(runId, success, result, error); - } + @Override + public int hashCode() { + return Objects.hash(runId, success, result, error); + } - @Override - public ServerBoundPacket asPacket(Serializer serializer) { - return new ServerBoundPacket( - ServerBoundPacketType.GET_RESULT_REPLY, - serializer.serializeTree(this) - ); - } + @Override + public ServerBoundPacket asPacket(Serializer serializer) { + return new ServerBoundPacket( + ServerBoundPacketType.GET_RESULT_REPLY, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReply.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReply.java index 136ab27d1..ada91a1de 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReply.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReply.java @@ -10,107 +10,105 @@ import java.util.UUID; import javax.annotation.Nullable; -/** - * A reply containing the runner's current status. - */ +/** A reply containing the runner's current status. */ public class GetStatusReply implements ServerBound { - private final String info; - @Nullable - private final String versionHash; - @Nullable - private final String benchHash; - private final boolean resultAvailable; - private final Status status; - @Nullable - private final UUID runId; - @Nullable - private final LinesWithOffset lastOutputLines; + private final String info; + @Nullable private final String versionHash; + @Nullable private final String benchHash; + private final boolean resultAvailable; + private final Status status; + @Nullable private final UUID runId; + @Nullable private final LinesWithOffset lastOutputLines; - @JsonCreator - public GetStatusReply( - @JsonProperty(required = true) String info, - @Nullable String versionHash, - @Nullable String benchHash, - @JsonProperty(required = true) boolean resultAvailable, - @JsonProperty(required = true) Status status, - @Nullable UUID runId, - @Nullable LinesWithOffset lastOutputLines - ) { - this.info = info; - this.versionHash = versionHash; - this.benchHash = benchHash; - this.resultAvailable = resultAvailable; - this.status = status; - this.runId = runId; - this.lastOutputLines = lastOutputLines; - } + @JsonCreator + public GetStatusReply( + @JsonProperty(required = true) String info, + @Nullable String versionHash, + @Nullable String benchHash, + @JsonProperty(required = true) boolean resultAvailable, + @JsonProperty(required = true) Status status, + @Nullable UUID runId, + @Nullable LinesWithOffset lastOutputLines) { + this.info = info; + this.versionHash = versionHash; + this.benchHash = benchHash; + this.resultAvailable = resultAvailable; + this.status = status; + this.runId = runId; + this.lastOutputLines = lastOutputLines; + } - public String getInfo() { - return info; - } + public String getInfo() { + return info; + } - public Optional getVersionHash() { - return Optional.ofNullable(versionHash); - } + public Optional getVersionHash() { + return Optional.ofNullable(versionHash); + } - public Optional getBenchHash() { - return Optional.ofNullable(benchHash); - } + public Optional getBenchHash() { + return Optional.ofNullable(benchHash); + } - public boolean isResultAvailable() { - return resultAvailable; - } + public boolean isResultAvailable() { + return resultAvailable; + } - public Status getStatus() { - return status; - } + public Status getStatus() { + return status; + } - public Optional getRunId() { - return Optional.ofNullable(runId); - } + public Optional getRunId() { + return Optional.ofNullable(runId); + } - public Optional getLastOutputLines() { - return Optional.ofNullable(lastOutputLines); - } + public Optional getLastOutputLines() { + return Optional.ofNullable(lastOutputLines); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - GetStatusReply that = (GetStatusReply) o; - return resultAvailable == that.resultAvailable && - info.equals(that.info) && - Objects.equals(benchHash, that.benchHash) && - status.equals(that.status) && - Objects.equals(runId, that.runId); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GetStatusReply that = (GetStatusReply) o; + return resultAvailable == that.resultAvailable + && info.equals(that.info) + && Objects.equals(benchHash, that.benchHash) + && status.equals(that.status) + && Objects.equals(runId, that.runId); + } - @Override - public int hashCode() { - return Objects.hash(info, benchHash, resultAvailable, status, runId); - } + @Override + public int hashCode() { + return Objects.hash(info, benchHash, resultAvailable, status, runId); + } - @Override - public ServerBoundPacket asPacket(Serializer serializer) { - return new ServerBoundPacket( - ServerBoundPacketType.GET_STATUS_REPLY, - serializer.serializeTree(this) - ); - } + @Override + public ServerBoundPacket asPacket(Serializer serializer) { + return new ServerBoundPacket( + ServerBoundPacketType.GET_STATUS_REPLY, serializer.serializeTree(this)); + } - @Override - public String toString() { - return "GetStatusReply{" + - "versionHash='" + versionHash + '\'' + - ", benchHash='" + benchHash + '\'' + - ", resultAvailable=" + resultAvailable + - ", status=" + status + - ", runId=" + runId + - '}'; - } + @Override + public String toString() { + return "GetStatusReply{" + + "versionHash='" + + versionHash + + '\'' + + ", benchHash='" + + benchHash + + '\'' + + ", resultAvailable=" + + resultAvailable + + ", status=" + + status + + ", runId=" + + runId + + '}'; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRun.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRun.java index a60cb353d..53319c00d 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRun.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRun.java @@ -8,11 +8,8 @@ */ public class RequestRun implements ServerBound { - @Override - public ServerBoundPacket asPacket(Serializer serializer) { - return new ServerBoundPacket( - ServerBoundPacketType.REQUEST_RUN, - serializer.serializeTree(this) - ); - } + @Override + public ServerBoundPacket asPacket(Serializer serializer) { + return new ServerBoundPacket(ServerBoundPacketType.REQUEST_RUN, serializer.serializeTree(this)); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBound.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBound.java index b0b0d116e..d3402d053 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBound.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBound.java @@ -2,17 +2,14 @@ import de.aaaaaaah.velcom.shared.protocol.serialization.Serializer; -/** - * Packets that can be converted into a {@link ServerBoundPacket}. - */ +/** Packets that can be converted into a {@link ServerBoundPacket}. */ public interface ServerBound { - /** - * Convert this packet to a {@link ServerBoundPacket}. - * - * @param serializer the JSON serializer instance to use - * @return the {@link ServerBoundPacket} - */ - ServerBoundPacket asPacket(Serializer serializer); - + /** + * Convert this packet to a {@link ServerBoundPacket}. + * + * @param serializer the JSON serializer instance to use + * @return the {@link ServerBoundPacket} + */ + ServerBoundPacket asPacket(Serializer serializer); } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacket.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacket.java index 40475e393..29cedb868 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacket.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacket.java @@ -5,54 +5,47 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.Objects; -/** - * A packet that can be sent to the backend by the runner. - */ +/** A packet that can be sent to the backend by the runner. */ public class ServerBoundPacket { - private final ServerBoundPacketType type; - private final JsonNode data; - - @JsonCreator - public ServerBoundPacket( - @JsonProperty(required = true) ServerBoundPacketType type, - @JsonProperty(required = true) JsonNode data - ) { - this.type = type; - this.data = data; - } - - public ServerBoundPacketType getType() { - return type; - } - - public JsonNode getData() { - return data; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ServerBoundPacket that = (ServerBoundPacket) o; - return type == that.type && - data.equals(that.data); - } - - @Override - public int hashCode() { - return Objects.hash(type, data); - } - - @Override - public String toString() { - return "ServerBoundPacket{" + - "type=" + type + - ", data=" + data + - '}'; - } + private final ServerBoundPacketType type; + private final JsonNode data; + + @JsonCreator + public ServerBoundPacket( + @JsonProperty(required = true) ServerBoundPacketType type, + @JsonProperty(required = true) JsonNode data) { + this.type = type; + this.data = data; + } + + public ServerBoundPacketType getType() { + return type; + } + + public JsonNode getData() { + return data; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerBoundPacket that = (ServerBoundPacket) o; + return type == that.type && data.equals(that.data); + } + + @Override + public int hashCode() { + return Objects.hash(type, data); + } + + @Override + public String toString() { + return "ServerBoundPacket{" + "type=" + type + ", data=" + data + '}'; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketType.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketType.java index f5b8136ce..76919d1d8 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketType.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketType.java @@ -3,56 +3,51 @@ import com.fasterxml.jackson.annotation.JsonValue; import java.util.Optional; -/** - * The types of packets that can be sent to the backend by the runner. - */ +/** The types of packets that can be sent to the backend by the runner. */ public enum ServerBoundPacketType { - ABORT_RUN_REPLY("abort_run_reply", AbortRunReply.class), - CLEAR_RESULT_REPLY("clear_result_reply", ClearResultReply.class), - GET_RESULT_REPLY("get_result_reply", GetResultReply.class), - GET_STATUS_REPLY("get_status_reply", GetStatusReply.class), - REQUEST_RUN("request_run", RequestRun.class); - - private final String type; - private final Class dataClass; - - ServerBoundPacketType(String type, Class dataClass) { - this.type = type; - this.dataClass = dataClass; - } - - static Optional ofType(String type) { - for (ServerBoundPacketType value : ServerBoundPacketType.values()) { - if (value.type.equals(type)) { - return Optional.of(value); - } - } - return Optional.empty(); - } - - static Optional ofClass(Class theClass) { - for (ServerBoundPacketType value : ServerBoundPacketType.values()) { - if (value.dataClass.equals(theClass)) { - return Optional.of(value); - } - } - return Optional.empty(); - } - - @JsonValue - public String getType() { - return type; - } - - public Class getDataClass() { - return dataClass; - } - - @Override - public String toString() { - return "ServerBoundPacketType{" + - "type='" + type + '\'' + - ", dataClass=" + dataClass + - '}'; - } + ABORT_RUN_REPLY("abort_run_reply", AbortRunReply.class), + CLEAR_RESULT_REPLY("clear_result_reply", ClearResultReply.class), + GET_RESULT_REPLY("get_result_reply", GetResultReply.class), + GET_STATUS_REPLY("get_status_reply", GetStatusReply.class), + REQUEST_RUN("request_run", RequestRun.class); + + private final String type; + private final Class dataClass; + + ServerBoundPacketType(String type, Class dataClass) { + this.type = type; + this.dataClass = dataClass; + } + + static Optional ofType(String type) { + for (ServerBoundPacketType value : ServerBoundPacketType.values()) { + if (value.type.equals(type)) { + return Optional.of(value); + } + } + return Optional.empty(); + } + + static Optional ofClass(Class theClass) { + for (ServerBoundPacketType value : ServerBoundPacketType.values()) { + if (value.dataClass.equals(theClass)) { + return Optional.of(value); + } + } + return Optional.empty(); + } + + @JsonValue + public String getType() { + return type; + } + + public Class getDataClass() { + return dataClass; + } + + @Override + public String toString() { + return "ServerBoundPacketType{" + "type='" + type + '\'' + ", dataClass=" + dataClass + '}'; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/State.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/State.java index 1e04b2938..bf20af81d 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/State.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/State.java @@ -1,32 +1,28 @@ package de.aaaaaaah.velcom.shared.protocol.statemachine; -/** - * A state for the {@link StateMachine}. - */ +/** A state for the {@link StateMachine}. */ public interface State { - /** - * This function is called whenever the state is entered. It is also called on the first state - * when a {@link StateMachine} is initialized. It exists because large amounts of time may pass - * between creation of the state object and the {@link StateMachine} switching to said object. - */ - default void onEnter() { - } + /** + * This function is called whenever the state is entered. It is also called on the first state + * when a {@link StateMachine} is initialized. It exists because large amounts of time may pass + * between creation of the state object and the {@link StateMachine} switching to said object. + */ + default void onEnter() {} - /** - * This function is called whenever the state is exited. It is also called when the {@link - * StateMachine} is stopped. It can be used to clean up resources like files or timers. - */ - default void onExit() { - } + /** + * This function is called whenever the state is exited. It is also called when the {@link + * StateMachine} is stopped. It can be used to clean up resources like files or timers. + */ + default void onExit() {} - /** - * If a state is resting, it can be switched out of at any time using the {@link - * StateMachine#switchFromRestingState(State)} function. - * - * @return whether this state is a resting state - */ - default boolean isResting() { - return false; - } + /** + * If a state is resting, it can be switched out of at any time using the {@link + * StateMachine#switchFromRestingState(State)} function. + * + * @return whether this state is a resting state + */ + default boolean isResting() { + return false; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachine.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachine.java index 96e58931e..54ce566fe 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachine.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachine.java @@ -13,114 +13,112 @@ */ public class StateMachine { - // This lock protects stopped and currentState. - private final Lock lock; - private final Condition inRestingStateOrStopped; - private boolean stopped; - @Nullable - private S currentState; - - public StateMachine(S initialState) { - lock = new ReentrantLock(true); - inRestingStateOrStopped = lock.newCondition(); - stopped = false; - - switchUnconditionally(initialState); - } - - /** - * Change the state machine's current state in a threadsafe manner. - * - * @param modifier the function to decide the new state based on the current state. It receives - * the current state as its argument and its return value will become the new state. - * @return true if the state was switched successfully, false otherwise - */ - public boolean changeCurrentState(Function modifier) { - lock.lock(); - try { - if (stopped) { - return false; - } - - return switchUnconditionally(modifier.apply(currentState)); - } finally { - lock.unlock(); - } - } - - /** - * Switch to a new state, no matter what state the state machine is currently in. - * - * @param newState the state to switch to - * @return true if the state was switched successfully, false otherwise - */ - private boolean switchUnconditionally(S newState) { - lock.lock(); - try { - if (stopped) { - return false; - } - - // Intentionally comparing objects via == instead of using .equals() - if (currentState != newState) { - if (currentState != null) { - currentState.onExit(); - } - - currentState = newState; - - if (currentState != null) { - currentState.onEnter(); - } - - inRestingStateOrStopped.signalAll(); - } - - return true; - } finally { - lock.unlock(); - } - } - - /** - * Switch to a new state as soon as the state machine enters a resting state again. - * - * @param newState the state to switch to - * @return true if the state was switched successfully, false otherwise - * @throws InterruptedException if the thread got interrupted while waiting for the state machine - * to enter a resting state - */ - public boolean switchFromRestingState(S newState) throws InterruptedException { - lock.lock(); - try { - while (!(stopped || currentState == null || currentState.isResting())) { - inRestingStateOrStopped.await(); - } - - return switchUnconditionally(newState); - } finally { - lock.unlock(); - } - } - - /** - * Stop the state machine. After this function is called, all currently blocking calls to switch - * the state will return and all further attempts at state switching become a noop. Also, the - * current state (if there is one) receives its {@link State#onExit()} call. - */ - public void stop() { - lock.lock(); - try { - // Ensure the current state receives its onExit() call - switchUnconditionally(null); - - // Ensure that no further state switch will succeed and all switches currently waiting - // for a resting state return. - stopped = true; - inRestingStateOrStopped.signalAll(); - } finally { - lock.unlock(); - } - } - + // This lock protects stopped and currentState. + private final Lock lock; + private final Condition inRestingStateOrStopped; + private boolean stopped; + @Nullable private S currentState; + + public StateMachine(S initialState) { + lock = new ReentrantLock(true); + inRestingStateOrStopped = lock.newCondition(); + stopped = false; + + switchUnconditionally(initialState); + } + + /** + * Change the state machine's current state in a threadsafe manner. + * + * @param modifier the function to decide the new state based on the current state. It receives + * the current state as its argument and its return value will become the new state. + * @return true if the state was switched successfully, false otherwise + */ + public boolean changeCurrentState(Function modifier) { + lock.lock(); + try { + if (stopped) { + return false; + } + + return switchUnconditionally(modifier.apply(currentState)); + } finally { + lock.unlock(); + } + } + + /** + * Switch to a new state, no matter what state the state machine is currently in. + * + * @param newState the state to switch to + * @return true if the state was switched successfully, false otherwise + */ + private boolean switchUnconditionally(S newState) { + lock.lock(); + try { + if (stopped) { + return false; + } + + // Intentionally comparing objects via == instead of using .equals() + if (currentState != newState) { + if (currentState != null) { + currentState.onExit(); + } + + currentState = newState; + + if (currentState != null) { + currentState.onEnter(); + } + + inRestingStateOrStopped.signalAll(); + } + + return true; + } finally { + lock.unlock(); + } + } + + /** + * Switch to a new state as soon as the state machine enters a resting state again. + * + * @param newState the state to switch to + * @return true if the state was switched successfully, false otherwise + * @throws InterruptedException if the thread got interrupted while waiting for the state machine + * to enter a resting state + */ + public boolean switchFromRestingState(S newState) throws InterruptedException { + lock.lock(); + try { + while (!(stopped || currentState == null || currentState.isResting())) { + inRestingStateOrStopped.await(); + } + + return switchUnconditionally(newState); + } finally { + lock.unlock(); + } + } + + /** + * Stop the state machine. After this function is called, all currently blocking calls to switch + * the state will return and all further attempts at state switching become a noop. Also, the + * current state (if there is one) receives its {@link State#onExit()} call. + */ + public void stop() { + lock.lock(); + try { + // Ensure the current state receives its onExit() call + switchUnconditionally(null); + + // Ensure that no further state switch will succeed and all switches currently waiting + // for a resting state return. + stopped = true; + inRestingStateOrStopped.signalAll(); + } finally { + lock.unlock(); + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Either.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Either.java index 510d16206..3667c19d8 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Either.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Either.java @@ -13,132 +13,127 @@ */ public final class Either { - private final L left; - private final R right; - - private Either(L left, R right) { - this.left = left; - this.right = right; - } - - /** - * Create a new {@link Either} with a left value. - * - * @param left the left value - * @param the type of the left value - * @param the type of the right value - * @return the new {@link Either} - */ - public static Either ofLeft(L2 left) { - return new Either<>(Objects.requireNonNull(left), null); - } - - - /** - * Create a new {@link Either} with a right value. - * - * @param right the right value - * @param the type of the left value - * @param the type of the right value - * @return the new {@link Either} - */ - public static Either ofRight(R2 right) { - return new Either<>(null, Objects.requireNonNull(right)); - } - - /** - * @return whether this {@link Either} has a right value - */ - public boolean isRight() { - return right != null; - } - - /** - * @return whether this {@link Either} has a left value - */ - public boolean isLeft() { - return left != null; - } - - public Optional getLeft() { - return Optional.ofNullable(left); - } - - public Optional getRight() { - return Optional.ofNullable(right); - } - - /** - * Turn an {@link Either} into a value of another type using one function to convert a left value - * to the target type and one to convert a right value to the target type. - * - * @param left the function that creates a value of the target type from a left value - * @param right the function that creates a value of the target type from a right value - * @param the target type - * @return a value of the target type using either the left or right function - */ - public Result consume(Function left, Function right) { - return isLeft() ? left.apply(this.left) : right.apply(this.right); - } - - /** - * Map one of two functions over the {@link Either} depeding on whether it's left or right. - * - * @param left the function to map over a left value - * @param right the function to map over the right value - * @param the result left type - * @param the result right type - * @return the mapped-over {@link Either} - */ - public Either mapBoth(Function left, Function right) { - return consume( - l -> Either.ofLeft(left.apply(l)), - r -> Either.ofRight(right.apply(r)) - ); - } - - /** - * Map a function over the left value, if the {@link Either} is left. - * - * @param left the function to map over the left value - * @param the resulting left type - * @return the mapped-over {@link Either} - */ - public Either mapLeft(Function left) { - return mapBoth(left, it -> it); - } - - /** - * Map a function over the right value, if the {@link Either} is right. - * - * @param right the function to map over the right value - * @param the resulting right type - * @return the mapped-over {@link Either} - */ - public Either mapRight(Function right) { - return mapBoth(it -> it, right); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Either either = (Either) o; - return Objects.equals(left, either.left) && - Objects.equals(right, either.right); - } - - @Override - public int hashCode() { - return Objects.hash(left, right); - } - - @Override - public String toString() { - return consume(l -> "Either.ofLeft(" + l + ")", r -> "Either.ofRight(" + r + ")"); - } + private final L left; + private final R right; + + private Either(L left, R right) { + this.left = left; + this.right = right; + } + + /** + * Create a new {@link Either} with a left value. + * + * @param left the left value + * @param the type of the left value + * @param the type of the right value + * @return the new {@link Either} + */ + public static Either ofLeft(L2 left) { + return new Either<>(Objects.requireNonNull(left), null); + } + + /** + * Create a new {@link Either} with a right value. + * + * @param right the right value + * @param the type of the left value + * @param the type of the right value + * @return the new {@link Either} + */ + public static Either ofRight(R2 right) { + return new Either<>(null, Objects.requireNonNull(right)); + } + + /** + * @return whether this {@link Either} has a right value + */ + public boolean isRight() { + return right != null; + } + + /** + * @return whether this {@link Either} has a left value + */ + public boolean isLeft() { + return left != null; + } + + public Optional getLeft() { + return Optional.ofNullable(left); + } + + public Optional getRight() { + return Optional.ofNullable(right); + } + + /** + * Turn an {@link Either} into a value of another type using one function to convert a left value + * to the target type and one to convert a right value to the target type. + * + * @param left the function that creates a value of the target type from a left value + * @param right the function that creates a value of the target type from a right value + * @param the target type + * @return a value of the target type using either the left or right function + */ + public Result consume(Function left, Function right) { + return isLeft() ? left.apply(this.left) : right.apply(this.right); + } + + /** + * Map one of two functions over the {@link Either} depeding on whether it's left or right. + * + * @param left the function to map over a left value + * @param right the function to map over the right value + * @param the result left type + * @param the result right type + * @return the mapped-over {@link Either} + */ + public Either mapBoth(Function left, Function right) { + return consume(l -> Either.ofLeft(left.apply(l)), r -> Either.ofRight(right.apply(r))); + } + + /** + * Map a function over the left value, if the {@link Either} is left. + * + * @param left the function to map over the left value + * @param the resulting left type + * @return the mapped-over {@link Either} + */ + public Either mapLeft(Function left) { + return mapBoth(left, it -> it); + } + + /** + * Map a function over the right value, if the {@link Either} is right. + * + * @param right the function to map over the right value + * @param the resulting right type + * @return the mapped-over {@link Either} + */ + public Either mapRight(Function right) { + return mapBoth(it -> it, right); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Either either = (Either) o; + return Objects.equals(left, either.left) && Objects.equals(right, either.right); + } + + @Override + public int hashCode() { + return Objects.hash(left, right); + } + + @Override + public String toString() { + return consume(l -> "Either.ofLeft(" + l + ")", r -> "Either.ofRight(" + r + ")"); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/ExceptionHelper.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/ExceptionHelper.java index e7f849f1f..b3d1e7e58 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/ExceptionHelper.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/ExceptionHelper.java @@ -3,21 +3,19 @@ import java.io.PrintWriter; import java.io.StringWriter; -/** - * Helps with common exception operations. - */ +/** Helps with common exception operations. */ public class ExceptionHelper { - /** - * Returns the stacktrace of a {@link Throwable} as a String. - * - * @param throwable the throwable - * @return the stacktrace - */ - public static String getStackTrace(Throwable throwable) { - StringWriter stringWriter = new StringWriter(); - throwable.printStackTrace(new PrintWriter(stringWriter)); + /** + * Returns the stacktrace of a {@link Throwable} as a String. + * + * @param throwable the throwable + * @return the stacktrace + */ + public static String getStackTrace(Throwable throwable) { + StringWriter stringWriter = new StringWriter(); + throwable.printStackTrace(new PrintWriter(stringWriter)); - return stringWriter.toString(); - } + return stringWriter.toString(); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/FileHelper.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/FileHelper.java index 8a7523369..324cd18ad 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/FileHelper.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/FileHelper.java @@ -8,53 +8,54 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -/** - * Helps with common file I/O operations. - */ +/** Helps with common file I/O operations. */ public final class FileHelper { - private FileHelper() { - throw new UnsupportedOperationException("No"); - } + private FileHelper() { + throw new UnsupportedOperationException("No"); + } - /** - * Deletes a file or directory. - * - * @param path the path to the file to delete - * @throws IOException if an error occurs - */ - public static void deleteDirectoryOrFile(Path path) throws IOException { - if (Files.notExists(path)) { - return; - } + /** + * Deletes a file or directory. + * + * @param path the path to the file to delete + * @throws IOException if an error occurs + */ + public static void deleteDirectoryOrFile(Path path) throws IOException { + if (Files.notExists(path)) { + return; + } - Files.walkFileTree(path, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - file.toFile().setWritable(true); - Files.delete(file); - return FileVisitResult.CONTINUE; - } + Files.walkFileTree( + path, + new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + file.toFile().setWritable(true); + Files.delete(file); + return FileVisitResult.CONTINUE; + } - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } - /** - * Deletes a file or directory. - * - * @param path the path to the file to delete - * @throws UncheckedIOException wrapping an IOException if an error occurs - */ - public static void uncheckedDeleteDirectoryOrFile(Path path) throws UncheckedIOException { - try { - deleteDirectoryOrFile(path); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } + /** + * Deletes a file or directory. + * + * @param path the path to the file to delete + * @throws UncheckedIOException wrapping an IOException if an error occurs + */ + public static void uncheckedDeleteDirectoryOrFile(Path path) throws UncheckedIOException { + try { + deleteDirectoryOrFile(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/LinesWithOffset.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/LinesWithOffset.java index 29a6e4604..6cea35d1f 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/LinesWithOffset.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/LinesWithOffset.java @@ -8,26 +8,26 @@ */ public class LinesWithOffset { - private final int firstLineOffset; - private final List lines; + private final int firstLineOffset; + private final List lines; - /** - * If there are three lines and the last two are included in this object, the firstLineOffset will - * be one (totalLength - includedLength). - * - * @param firstLineOffset the offset of the first line. 0 based. - * @param lines the lines - */ - public LinesWithOffset(int firstLineOffset, List lines) { - this.firstLineOffset = firstLineOffset; - this.lines = lines; - } + /** + * If there are three lines and the last two are included in this object, the firstLineOffset will + * be one (totalLength - includedLength). + * + * @param firstLineOffset the offset of the first line. 0 based. + * @param lines the lines + */ + public LinesWithOffset(int firstLineOffset, List lines) { + this.firstLineOffset = firstLineOffset; + this.lines = lines; + } - public int getFirstLineOffset() { - return firstLineOffset; - } + public int getFirstLineOffset() { + return firstLineOffset; + } - public List getLines() { - return lines; - } + public List getLines() { + return lines; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/OSCheck.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/OSCheck.java index 48ec710c2..3b40bdd53 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/OSCheck.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/OSCheck.java @@ -1,18 +1,13 @@ package de.aaaaaaah.velcom.shared.util; -/** - * Check whether this program is running on a lame OS. - */ +/** Check whether this program is running on a lame OS. */ public final class OSCheck { - private OSCheck() { - throw new UnsupportedOperationException("No instantiation"); - } - - public static boolean isStupidWindows() { - return System.getProperty("os.name", "generic") - .toLowerCase() - .contains("windows"); - } + private OSCheck() { + throw new UnsupportedOperationException("No instantiation"); + } + public static boolean isStupidWindows() { + return System.getProperty("os.name", "generic").toLowerCase().contains("windows"); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Pair.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Pair.java index c501cdcfd..5a5986ed9 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Pair.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Pair.java @@ -5,8 +5,7 @@ /** * A simple key-value pair. * - *

- * This pair has a consistent {@link #equals(Object)} and {@link #hashCode()} method, if the two + *

This pair has a consistent {@link #equals(Object)} and {@link #hashCode()} method, if the two * element types have one. * * @param the type of the first element in the pair @@ -14,45 +13,41 @@ */ public class Pair { - private final A first; - private final B second; - - public Pair(A first, B second) { - this.first = first; - this.second = second; - } - - public A getFirst() { - return first; - } - - public B getSecond() { - return second; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Pair pair = (Pair) o; - return Objects.equals(first, pair.first) && - Objects.equals(second, pair.second); - } - - @Override - public int hashCode() { - return Objects.hash(first, second); - } - - @Override - public String toString() { - return "Pair{" + - "first=" + first + - ", second=" + second + - '}'; - } + private final A first; + private final B second; + + public Pair(A first, B second) { + this.first = first; + this.second = second; + } + + public A getFirst() { + return first; + } + + public B getSecond() { + return second; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pair pair = (Pair) o; + return Objects.equals(first, pair.first) && Objects.equals(second, pair.second); + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } + + @Override + public String toString() { + return "Pair{" + "first=" + first + ", second=" + second + '}'; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringHelper.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringHelper.java index bf5c16fd0..b717c0f25 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringHelper.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringHelper.java @@ -1,25 +1,24 @@ package de.aaaaaaah.velcom.shared.util; -/** - * A utility class for manipulating strings. - */ +/** A utility class for manipulating strings. */ public class StringHelper { - /** - * Wrap a string in quotation marks. Escape quotation marks and backslashes in the string to make - * the quotation unambiguous. Also escape some whitespace. - * - * @param string the string to wrap in quotationMarks - * @return the quoted (and escaped) string - */ - public static String quote(String string) { - String escaped = string - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\t", "\\t") - .replace("\b", "\\b"); + /** + * Wrap a string in quotation marks. Escape quotation marks and backslashes in the string to make + * the quotation unambiguous. Also escape some whitespace. + * + * @param string the string to wrap in quotationMarks + * @return the quoted (and escaped) string + */ + public static String quote(String string) { + String escaped = + string + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\b", "\\b"); - return "\"" + escaped + "\""; - } + return "\"" + escaped + "\""; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringOutputStream.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringOutputStream.java index aacdb36b5..bbe6b277a 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringOutputStream.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/StringOutputStream.java @@ -5,38 +5,36 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; -/** - * A synchronized {@link OutputStream} that writes to a String. - */ +/** A synchronized {@link OutputStream} that writes to a String. */ public class StringOutputStream extends OutputStream { - private final ByteArrayOutputStream byteArrayOutputStream; - - public StringOutputStream() { - this.byteArrayOutputStream = new ByteArrayOutputStream(); - } - - @Override - public synchronized void write(int b) { - byteArrayOutputStream.write(b); - } - - @Override - public synchronized void write(byte[] b) throws IOException { - byteArrayOutputStream.write(b); - } - - @Override - public synchronized void write(byte[] b, int off, int len) { - byteArrayOutputStream.write(b, off, len); - } - - /** - * Returns the underlying read string. - * - * @return the underlying string - */ - public synchronized String getString() { - return byteArrayOutputStream.toString(StandardCharsets.UTF_8); - } + private final ByteArrayOutputStream byteArrayOutputStream; + + public StringOutputStream() { + this.byteArrayOutputStream = new ByteArrayOutputStream(); + } + + @Override + public synchronized void write(int b) { + byteArrayOutputStream.write(b); + } + + @Override + public synchronized void write(byte[] b) throws IOException { + byteArrayOutputStream.write(b); + } + + @Override + public synchronized void write(byte[] b, int off, int len) { + byteArrayOutputStream.write(b, off, len); + } + + /** + * Returns the underlying read string. + * + * @return the underlying string + */ + public synchronized String getString() { + return byteArrayOutputStream.toString(StandardCharsets.UTF_8); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Timeout.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Timeout.java index b4dff584c..de70b7bc0 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Timeout.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/Timeout.java @@ -11,58 +11,58 @@ */ public class Timeout { - private final Thread waitThread; - private final CompletableFuture future; - private volatile boolean cancelled; + private final Thread waitThread; + private final CompletableFuture future; + private volatile boolean cancelled; - private Timeout(Thread waitThread, CompletableFuture future) { - this.waitThread = waitThread; - this.future = future; + private Timeout(Thread waitThread, CompletableFuture future) { + this.waitThread = waitThread; + this.future = future; - cancelled = false; - } + cancelled = false; + } - /** - * Create a timeout that activates after a certain amount of time. - * - * @param duration How long to wait until the timeout activates - * @return the timeout - */ - public static Timeout after(Duration duration) { - CompletableFuture future = new CompletableFuture<>(); + /** + * Create a timeout that activates after a certain amount of time. + * + * @param duration How long to wait until the timeout activates + * @return the timeout + */ + public static Timeout after(Duration duration) { + CompletableFuture future = new CompletableFuture<>(); - Thread waitThread = new Thread(() -> { - try { - Thread.sleep(duration.toMillis()); - future.complete(null); - } catch (InterruptedException e) { - future.cancel(true); - } - }); + Thread waitThread = + new Thread( + () -> { + try { + Thread.sleep(duration.toMillis()); + future.complete(null); + } catch (InterruptedException e) { + future.cancel(true); + } + }); - return new Timeout(waitThread, future); - } + return new Timeout(waitThread, future); + } - /** - * Start the timeout. - */ - public void start() { - if (!cancelled) { - waitThread.start(); - } - } + /** Start the timeout. */ + public void start() { + if (!cancelled) { + waitThread.start(); + } + } - public CompletionStage getCompletionStage() { - return future; - } + public CompletionStage getCompletionStage() { + return future; + } - /** - * Cancel the timeout. Use this function instead of cancelling the {@link CompletionStage} - * returned by getCompletionStage directly. Can be called multiple times. - */ - public void cancel() { - cancelled = true; - waitThread.interrupt(); - future.cancel(true); - } + /** + * Cancel the timeout. Use this function instead of cancelling the {@link CompletionStage} + * returned by getCompletionStage directly. Can be called multiple times. + */ + public void cancel() { + cancelled = true; + waitThread.interrupt(); + future.cancel(true); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelper.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelper.java index aafae0923..ce4b91999 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelper.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelper.java @@ -6,62 +6,57 @@ import java.util.Map; import java.util.Set; -/** - * A small helper for reading and manipulating linux file permissions. - */ +/** A small helper for reading and manipulating linux file permissions. */ public class PermissionsHelper { - private static final Map INDEX_PERMISSION_MAP = Map.ofEntries( - Map.entry(0, PosixFilePermission.OTHERS_EXECUTE), - Map.entry(1, PosixFilePermission.OTHERS_WRITE), - Map.entry(2, PosixFilePermission.OTHERS_READ), - - Map.entry(3, PosixFilePermission.GROUP_EXECUTE), - Map.entry(4, PosixFilePermission.GROUP_WRITE), - Map.entry(5, PosixFilePermission.GROUP_READ), - - Map.entry(6, PosixFilePermission.OWNER_EXECUTE), - Map.entry(7, PosixFilePermission.OWNER_WRITE), - Map.entry(8, PosixFilePermission.OWNER_READ) - ); - - /** - * Converts an file mode int to a set of {@link java.nio.file.attribute.PosixFilePermission}s. - * - * @param mode the file mode (e.g. 644, 744, etc.) - * @return the equivalent posix file permissions - */ - public static Set fromOctal(int mode) { - Set permissions = EnumSet.noneOf(PosixFilePermission.class); - - for (int i = 0; i < INDEX_PERMISSION_MAP.size(); i++) { - if (isBitSet(i, mode)) { - permissions.add(INDEX_PERMISSION_MAP.get(i)); - } - } - - return permissions; - } - - /** - * Converts a set of {@link java.nio.file.attribute.PosixFilePermission}s to the octal posix - * mode. - * - * @param permissions the permissions - * @return the octal posix mode - */ - public static int toOctal(Collection permissions) { - int result = 0; - - for (int i = 0; i < INDEX_PERMISSION_MAP.size(); i++) { - if (permissions.contains(INDEX_PERMISSION_MAP.get(i))) { - result |= 1 << i; - } - } - return result; - } - - private static boolean isBitSet(int offset, int number) { - return ((number >> offset) & 0x1) == 1; - } + private static final Map INDEX_PERMISSION_MAP = + Map.ofEntries( + Map.entry(0, PosixFilePermission.OTHERS_EXECUTE), + Map.entry(1, PosixFilePermission.OTHERS_WRITE), + Map.entry(2, PosixFilePermission.OTHERS_READ), + Map.entry(3, PosixFilePermission.GROUP_EXECUTE), + Map.entry(4, PosixFilePermission.GROUP_WRITE), + Map.entry(5, PosixFilePermission.GROUP_READ), + Map.entry(6, PosixFilePermission.OWNER_EXECUTE), + Map.entry(7, PosixFilePermission.OWNER_WRITE), + Map.entry(8, PosixFilePermission.OWNER_READ)); + + /** + * Converts an file mode int to a set of {@link java.nio.file.attribute.PosixFilePermission}s. + * + * @param mode the file mode (e.g. 644, 744, etc.) + * @return the equivalent posix file permissions + */ + public static Set fromOctal(int mode) { + Set permissions = EnumSet.noneOf(PosixFilePermission.class); + + for (int i = 0; i < INDEX_PERMISSION_MAP.size(); i++) { + if (isBitSet(i, mode)) { + permissions.add(INDEX_PERMISSION_MAP.get(i)); + } + } + + return permissions; + } + + /** + * Converts a set of {@link java.nio.file.attribute.PosixFilePermission}s to the octal posix mode. + * + * @param permissions the permissions + * @return the octal posix mode + */ + public static int toOctal(Collection permissions) { + int result = 0; + + for (int i = 0; i < INDEX_PERMISSION_MAP.size(); i++) { + if (permissions.contains(INDEX_PERMISSION_MAP.get(i))) { + result |= 1 << i; + } + } + return result; + } + + private static boolean isBitSet(int offset, int number) { + return ((number >> offset) & 0x1) == 1; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/TarHelper.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/TarHelper.java index 65e7b4ae3..f8387a932 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/TarHelper.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/compression/TarHelper.java @@ -8,41 +8,36 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.utils.IOUtils; -/** - * Helps with unpacking tar files. - */ +/** Helps with unpacking tar files. */ public class TarHelper { - /** - * Unpacks a tape archive. - * - * @param tarFile the tar file - * @param targetDir the target directory to unpack it to - * @throws IOException if an error occurs - */ - public static void untar(Path tarFile, Path targetDir) throws IOException { - try (InputStream fileInput = Files.newInputStream(tarFile); - TarArchiveInputStream inputStream = new TarArchiveInputStream(fileInput)) { + /** + * Unpacks a tape archive. + * + * @param tarFile the tar file + * @param targetDir the target directory to unpack it to + * @throws IOException if an error occurs + */ + public static void untar(Path tarFile, Path targetDir) throws IOException { + try (InputStream fileInput = Files.newInputStream(tarFile); + TarArchiveInputStream inputStream = new TarArchiveInputStream(fileInput)) { - TarArchiveEntry entry; - while ((entry = inputStream.getNextTarEntry()) != null) { - Path entryPath = targetDir.resolve(Path.of(entry.getName())); - if (entry.isDirectory()) { - Files.createDirectories(entryPath); - } else { - if (Files.notExists(entryPath.getParent())) { - Files.createDirectories(entryPath.getParent()); - } - Files.createFile(entryPath); - try (var fileOut = Files.newOutputStream(entryPath)) { - IOUtils.copy(inputStream, fileOut); - } - Files.setPosixFilePermissions( - entryPath, - PermissionsHelper.fromOctal(entry.getMode()) - ); - } - } - } - } + TarArchiveEntry entry; + while ((entry = inputStream.getNextTarEntry()) != null) { + Path entryPath = targetDir.resolve(Path.of(entry.getName())); + if (entry.isDirectory()) { + Files.createDirectories(entryPath); + } else { + if (Files.notExists(entryPath.getParent())) { + Files.createDirectories(entryPath.getParent()); + } + Files.createFile(entryPath); + try (var fileOut = Files.newOutputStream(entryPath)) { + IOUtils.copy(inputStream, fileOut); + } + Files.setPosixFilePermissions(entryPath, PermissionsHelper.fromOctal(entry.getMode())); + } + } + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/DaemonThreadFactory.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/DaemonThreadFactory.java index 2c9bec818..592db6e52 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/DaemonThreadFactory.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/DaemonThreadFactory.java @@ -3,15 +3,13 @@ import java.util.concurrent.ThreadFactory; import javax.annotation.Nonnull; -/** - * A {@link ThreadFactory} that returns daemon threads. - */ +/** A {@link ThreadFactory} that returns daemon threads. */ public class DaemonThreadFactory implements ThreadFactory { - @Override - public Thread newThread(@Nonnull Runnable r) { - Thread thread = new Thread(r); - thread.setDaemon(true); - return thread; - } + @Override + public Thread newThread(@Nonnull Runnable r) { + Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutor.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutor.java index 73b4cef06..c0cbbf779 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutor.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutor.java @@ -14,113 +14,110 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** - * A small utility for executing programs. - */ +/** A small utility for executing programs. */ public class ProgramExecutor { - private static final Logger LOGGER = LoggerFactory.getLogger(ProgramExecutor.class); - - // If multiple ProgramExecutors are used, could block the common fork join pool completely. - // We occupy 2 threads per execution (draining stderr and stdio) - // Using a cached thread pool allows the executor to scale, while also trimming down unused - // tasks after some delay specified in the JDK method (currently 60 seconds). - // As the threads are daemons, we do not need to shut down the pool at all. If nobody waits for - // the result, the threads will die as well, otherwise they will be kept alive and things work - // as expected. - private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool( - new DaemonThreadFactory() - ); - - private final long timeToForceKillMillis; - - public ProgramExecutor() { - this.timeToForceKillMillis = TimeUnit.SECONDS.toMillis(10); - } - - public ProgramExecutor(long timeToForceKillMillis) { - this.timeToForceKillMillis = timeToForceKillMillis; - } - - /** - * Executes a program using the passed command. If the future is cancelled with - * interruption, the process is forcefully killed. - * - * @param command the command to execute - * @return a future representing the result - */ - public StreamsProcessOutput execute(String... command) { - StringOutputStream stdOutStream = new StringOutputStream(); - StringOutputStream stdErrStream = new StringOutputStream(); - - FutureTask futureTask = new FutureTask<>(() -> { - Instant startTime = Instant.now(); - - Process process = new ProcessBuilder(command).start(); - - CompletableFuture stdOut = readOutput(stdOutStream, process::getInputStream); - CompletableFuture stdErr = readOutput(stdErrStream, process::getErrorStream); - - try { - int exitCode = process.waitFor(); - - return new ProgramResult( - exitCode, - stdOut.get(), - stdErr.get(), - Duration.between(startTime, Instant.now()) - ); - } catch (InterruptedException e) { - process.toHandle().destroy(); - - try { - process.waitFor(timeToForceKillMillis, TimeUnit.MILLISECONDS); - } catch (InterruptedException ignored) { - LOGGER.warn( - "Interrupted while waiting for process to gracefully shutdown. Killing it"); - } - // Only kill it if it is still alive - if (process.toHandle().isAlive()) { - LOGGER.debug("Waited " + timeToForceKillMillis + " killing it"); - - process.toHandle().descendants().forEach(ProcessHandle::destroyForcibly); - process.toHandle().destroyForcibly(); - } - - throw new CancellationException("Killed process"); - } - }); - - return new WaitingFutureTask<>(futureTask, stdOutStream, stdErrStream); - } - - private CompletableFuture readOutput(StringOutputStream output, - UncheckedSupplier input) { - return CompletableFuture - .supplyAsync(asUnchecked(() -> { - try (InputStream inputStream = input.get()) { - inputStream.transferTo(output); - } - return output.getString(); - }), - EXECUTOR - ) - .exceptionally(throwable -> output.getString()); - } - - private Supplier asUnchecked(UncheckedSupplier supplier) { - return () -> { - try { - return supplier.get(); - } catch (Exception exception) { - throw new RuntimeException(exception); - } - }; - } - - private interface UncheckedSupplier { - - T get() throws Exception; - } - + private static final Logger LOGGER = LoggerFactory.getLogger(ProgramExecutor.class); + + // If multiple ProgramExecutors are used, could block the common fork join pool completely. + // We occupy 2 threads per execution (draining stderr and stdio) + // Using a cached thread pool allows the executor to scale, while also trimming down unused + // tasks after some delay specified in the JDK method (currently 60 seconds). + // As the threads are daemons, we do not need to shut down the pool at all. If nobody waits for + // the result, the threads will die as well, otherwise they will be kept alive and things work + // as expected. + private static final ExecutorService EXECUTOR = + Executors.newCachedThreadPool(new DaemonThreadFactory()); + + private final long timeToForceKillMillis; + + public ProgramExecutor() { + this.timeToForceKillMillis = TimeUnit.SECONDS.toMillis(10); + } + + public ProgramExecutor(long timeToForceKillMillis) { + this.timeToForceKillMillis = timeToForceKillMillis; + } + + /** + * Executes a program using the passed command. If the future is cancelled with + * interruption, the process is forcefully killed. + * + * @param command the command to execute + * @return a future representing the result + */ + public StreamsProcessOutput execute(String... command) { + StringOutputStream stdOutStream = new StringOutputStream(); + StringOutputStream stdErrStream = new StringOutputStream(); + + FutureTask futureTask = + new FutureTask<>( + () -> { + Instant startTime = Instant.now(); + + Process process = new ProcessBuilder(command).start(); + + CompletableFuture stdOut = readOutput(stdOutStream, process::getInputStream); + CompletableFuture stdErr = readOutput(stdErrStream, process::getErrorStream); + + try { + int exitCode = process.waitFor(); + + return new ProgramResult( + exitCode, + stdOut.get(), + stdErr.get(), + Duration.between(startTime, Instant.now())); + } catch (InterruptedException e) { + process.toHandle().destroy(); + + try { + process.waitFor(timeToForceKillMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) { + LOGGER.warn( + "Interrupted while waiting for process to gracefully shutdown. Killing it"); + } + // Only kill it if it is still alive + if (process.toHandle().isAlive()) { + LOGGER.debug("Waited " + timeToForceKillMillis + " killing it"); + + process.toHandle().descendants().forEach(ProcessHandle::destroyForcibly); + process.toHandle().destroyForcibly(); + } + + throw new CancellationException("Killed process"); + } + }); + + return new WaitingFutureTask<>(futureTask, stdOutStream, stdErrStream); + } + + private CompletableFuture readOutput( + StringOutputStream output, UncheckedSupplier input) { + return CompletableFuture.supplyAsync( + asUnchecked( + () -> { + try (InputStream inputStream = input.get()) { + inputStream.transferTo(output); + } + return output.getString(); + }), + EXECUTOR) + .exceptionally(throwable -> output.getString()); + } + + private Supplier asUnchecked(UncheckedSupplier supplier) { + return () -> { + try { + return supplier.get(); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + }; + } + + private interface UncheckedSupplier { + + T get() throws Exception; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramResult.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramResult.java index 0c55d9d15..73176390d 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramResult.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/ProgramResult.java @@ -2,36 +2,34 @@ import java.time.Duration; -/** - * The result of executing a program. - */ +/** The result of executing a program. */ public class ProgramResult { - private final int exitCode; - private final String stdOut; - private final String stdErr; - private final Duration runtime; - - public ProgramResult(int exitCode, String stdOut, String stdErr, Duration runtime) { - this.exitCode = exitCode; - this.stdOut = stdOut; - this.stdErr = stdErr; - this.runtime = runtime; - } - - public int getExitCode() { - return exitCode; - } - - public String getStdOut() { - return stdOut; - } - - public String getStdErr() { - return stdErr; - } - - public Duration getRuntime() { - return runtime; - } + private final int exitCode; + private final String stdOut; + private final String stdErr; + private final Duration runtime; + + public ProgramResult(int exitCode, String stdOut, String stdErr, Duration runtime) { + this.exitCode = exitCode; + this.stdOut = stdOut; + this.stdErr = stdErr; + this.runtime = runtime; + } + + public int getExitCode() { + return exitCode; + } + + public String getStdOut() { + return stdOut; + } + + public String getStdErr() { + return stdErr; + } + + public Duration getRuntime() { + return runtime; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/StreamsProcessOutput.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/StreamsProcessOutput.java index d7f04029b..53e0e5781 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/StreamsProcessOutput.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/StreamsProcessOutput.java @@ -10,13 +10,13 @@ */ public interface StreamsProcessOutput extends Future { - /** - * @return a snapshot of the current standard output of the process - */ - String getCurrentStdOut(); + /** + * @return a snapshot of the current standard output of the process + */ + String getCurrentStdOut(); - /** - * @return a snapshot of the current standard error - */ - String getCurrentStdErr(); + /** + * @return a snapshot of the current standard error + */ + String getCurrentStdErr(); } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/WaitingFutureTask.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/WaitingFutureTask.java index 84ef6b014..9a049e320 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/WaitingFutureTask.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/execution/WaitingFutureTask.java @@ -15,60 +15,60 @@ */ class WaitingFutureTask implements Future, StreamsProcessOutput { - private final Thread worker; - private final FutureTask underlying; - private final StringOutputStream stdErr; - private final StringOutputStream stdOut; + private final Thread worker; + private final FutureTask underlying; + private final StringOutputStream stdErr; + private final StringOutputStream stdOut; - public WaitingFutureTask(FutureTask task, StringOutputStream stdOut, - StringOutputStream stdErr) { - this.underlying = task; - this.stdOut = stdOut; - this.stdErr = stdErr; - this.worker = new Thread(underlying, "MyFutureTask worker"); - this.worker.start(); - } + public WaitingFutureTask( + FutureTask task, StringOutputStream stdOut, StringOutputStream stdErr) { + this.underlying = task; + this.stdOut = stdOut; + this.stdErr = stdErr; + this.worker = new Thread(underlying, "MyFutureTask worker"); + this.worker.start(); + } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return underlying.cancel(mayInterruptIfRunning); - } + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return underlying.cancel(mayInterruptIfRunning); + } - @Override - public boolean isCancelled() { - return underlying.isCancelled(); - } + @Override + public boolean isCancelled() { + return underlying.isCancelled(); + } - @Override - public boolean isDone() { - return underlying.isDone(); - } + @Override + public boolean isDone() { + return underlying.isDone(); + } - @Override - public T get() throws InterruptedException, ExecutionException { - if (isCancelled()) { - worker.join(); - } - return underlying.get(); - } + @Override + public T get() throws InterruptedException, ExecutionException { + if (isCancelled()) { + worker.join(); + } + return underlying.get(); + } - @Override - public T get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - if (isCancelled()) { - worker.join(unit.toMillis(timeout)); - throw new CancellationException("Execution cancelled, thread died"); - } - return underlying.get(timeout, unit); - } + @Override + public T get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + if (isCancelled()) { + worker.join(unit.toMillis(timeout)); + throw new CancellationException("Execution cancelled, thread died"); + } + return underlying.get(timeout, unit); + } - @Override - public String getCurrentStdOut() { - return stdOut.getString(); - } + @Override + public String getCurrentStdOut() { + return stdOut.getString(); + } - @Override - public String getCurrentStdErr() { - return stdErr.getString(); - } + @Override + public String getCurrentStdErr() { + return stdErr.getString(); + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfo.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfo.java index a121e49f3..9df8d2180 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfo.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfo.java @@ -2,74 +2,78 @@ import java.util.Objects; -/** - * Information about a single virtual core. - */ +/** Information about a single virtual core. */ public class CoreInfo { - private final int virtualCoreId; - private final int coreId; - private final int physicalId; - private final int cpuCores; - private final String model; + private final int virtualCoreId; + private final int coreId; + private final int physicalId; + private final int cpuCores; + private final String model; - public CoreInfo(int virtualCoreId, int coreId, int physicalId, int cpuCores, String model) { - this.virtualCoreId = virtualCoreId; - this.coreId = coreId; - this.physicalId = physicalId; - this.cpuCores = cpuCores; - this.model = model; - } + public CoreInfo(int virtualCoreId, int coreId, int physicalId, int cpuCores, String model) { + this.virtualCoreId = virtualCoreId; + this.coreId = coreId; + this.physicalId = physicalId; + this.cpuCores = cpuCores; + this.model = model; + } - public int getCoreId() { - return coreId; - } + public int getCoreId() { + return coreId; + } - public int getCpuCores() { - return cpuCores; - } + public int getCpuCores() { + return cpuCores; + } - public String getModel() { - return model; - } + public String getModel() { + return model; + } - public int getPhysicalId() { - return physicalId; - } + public int getPhysicalId() { + return physicalId; + } - public int getVirtualCoreId() { - return virtualCoreId; - } + public int getVirtualCoreId() { + return virtualCoreId; + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CoreInfo coreInfo = (CoreInfo) o; - return virtualCoreId == coreInfo.virtualCoreId && - coreId == coreInfo.coreId && - physicalId == coreInfo.physicalId && - cpuCores == coreInfo.cpuCores && - Objects.equals(model, coreInfo.model); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CoreInfo coreInfo = (CoreInfo) o; + return virtualCoreId == coreInfo.virtualCoreId + && coreId == coreInfo.coreId + && physicalId == coreInfo.physicalId + && cpuCores == coreInfo.cpuCores + && Objects.equals(model, coreInfo.model); + } - @Override - public int hashCode() { - return Objects.hash(virtualCoreId, coreId, physicalId, cpuCores, model); - } + @Override + public int hashCode() { + return Objects.hash(virtualCoreId, coreId, physicalId, cpuCores, model); + } - @Override - public String toString() { - return "CoreInfo{" + - "virtualCoreId=" + virtualCoreId + - ", coreId=" + coreId + - ", physicalId=" + physicalId + - ", cpuCores=" + cpuCores + - ", model='" + model + '\'' + - '}'; - } + @Override + public String toString() { + return "CoreInfo{" + + "virtualCoreId=" + + virtualCoreId + + ", coreId=" + + coreId + + ", physicalId=" + + physicalId + + ", cpuCores=" + + cpuCores + + ", model='" + + model + + '\'' + + '}'; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParser.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParser.java index c0210cd65..4be3ab2ef 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParser.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParser.java @@ -5,70 +5,64 @@ import java.util.Optional; import java.util.stream.Collectors; -/** - * A small parser for the /proc/cpuinfo file format. - */ +/** A small parser for the /proc/cpuinfo file format. */ class CoreInfoParser { - /** - * Extracts the core information out of the cpu info file. - * - * @param input the input lines - * @return the resulting core infos - */ - public List coreInfos(List input) { - return toCpuHunks(input).stream() - .map(this::hunkToCoreInfo) - .collect(Collectors.toList()); - } + /** + * Extracts the core information out of the cpu info file. + * + * @param input the input lines + * @return the resulting core infos + */ + public List coreInfos(List input) { + return toCpuHunks(input).stream().map(this::hunkToCoreInfo).collect(Collectors.toList()); + } - private List> toCpuHunks(List input) { - List> hunks = new ArrayList<>(); - List current = new ArrayList<>(); + private List> toCpuHunks(List input) { + List> hunks = new ArrayList<>(); + List current = new ArrayList<>(); - for (String line : input) { - if (line.isBlank()) { - hunks.add(current); - current = new ArrayList<>(); - } else { - current.add(line); - } - } + for (String line : input) { + if (line.isBlank()) { + hunks.add(current); + current = new ArrayList<>(); + } else { + current.add(line); + } + } - if (!current.isEmpty()) { - hunks.add(current); - } + if (!current.isEmpty()) { + hunks.add(current); + } - return hunks; - } + return hunks; + } - private CoreInfo hunkToCoreInfo(List hunk) { - return new CoreInfo( - findIntLine(hunk, "processor").orElse(-1), - findIntLine(hunk, "core id").orElse(-1), - findIntLine(hunk, "physical id").orElse(-1), - findIntLine(hunk, "cpu cores").orElse(-1), - findLine(hunk, "model name").orElse("N/A") - ); - } + private CoreInfo hunkToCoreInfo(List hunk) { + return new CoreInfo( + findIntLine(hunk, "processor").orElse(-1), + findIntLine(hunk, "core id").orElse(-1), + findIntLine(hunk, "physical id").orElse(-1), + findIntLine(hunk, "cpu cores").orElse(-1), + findLine(hunk, "model name").orElse("N/A")); + } - private Optional findLine(List hunk, String header) { - return hunk.stream() - .filter(it -> it.strip().startsWith(header)) - .map(it -> it.replaceAll(".+:", "").strip()) - .findFirst(); - } + private Optional findLine(List hunk, String header) { + return hunk.stream() + .filter(it -> it.strip().startsWith(header)) + .map(it -> it.replaceAll(".+:", "").strip()) + .findFirst(); + } - private Optional findIntLine(List hunk, String header) { - return findLine(hunk, header).flatMap(CoreInfoParser::parseInt); - } - - private static Optional parseInt(String input) { - try { - return Optional.of(Integer.parseInt(input)); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } + private Optional findIntLine(List hunk, String header) { + return findLine(hunk, header).flatMap(CoreInfoParser::parseInt); + } + private static Optional parseInt(String input) { + try { + return Optional.of(Integer.parseInt(input)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfo.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfo.java index e536dd54e..7646bbf19 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfo.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfo.java @@ -9,85 +9,76 @@ import java.util.Set; import java.util.stream.Collectors; -/** - * Information about one or more CPUs. - */ +/** Information about one or more CPUs. */ public class CpuInfo { - private final List coreInfos; + private final List coreInfos; - public CpuInfo(List coreInfos) { - this.coreInfos = new ArrayList<>(coreInfos); - } + public CpuInfo(List coreInfos) { + this.coreInfos = new ArrayList<>(coreInfos); + } - /** - * @return the amount of virtual cores. - */ - public int virtualCoreCount() { - return coreInfos.size(); - } + /** + * @return the amount of virtual cores. + */ + public int virtualCoreCount() { + return coreInfos.size(); + } - /** - * @return the amount of physical cores. - */ - public int physicalCoreCount() { - return coreInfos.stream() - .collect(groupingBy(CoreInfo::getPhysicalId, toList())) - .values() - .stream() - .mapToInt(it -> it.get(0).getCpuCores()) - .sum(); - } + /** + * @return the amount of physical cores. + */ + public int physicalCoreCount() { + return coreInfos.stream() + .collect(groupingBy(CoreInfo::getPhysicalId, toList())) + .values() + .stream() + .mapToInt(it -> it.get(0).getCpuCores()) + .sum(); + } - /** - * @return the amount of CPUs (might be more than one if you use some dedicated server hardware) - */ - public int cpuCount() { - return (int) coreInfos.stream() - .mapToInt(CoreInfo::getPhysicalId) - .distinct() - .count(); - } + /** + * @return the amount of CPUs (might be more than one if you use some dedicated server hardware) + */ + public int cpuCount() { + return (int) coreInfos.stream().mapToInt(CoreInfo::getPhysicalId).distinct().count(); + } - /** - * @return the model of all virtual cores (e.g. Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz) - */ - public Set coreModels() { - return coreInfos.stream() - .map(CoreInfo::getModel) - .collect(Collectors.toSet()); - } + /** + * @return the model of all virtual cores (e.g. Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz) + */ + public Set coreModels() { + return coreInfos.stream().map(CoreInfo::getModel).collect(Collectors.toSet()); + } - /** - * @return a human-readable string summarizing this info - */ - public String format() { - String models = String.join(",", coreModels()); - int n = virtualCoreCount(); - return String.format("%s (%d %s)", models, n, (n == 1) ? "thread" : "threads"); - } + /** + * @return a human-readable string summarizing this info + */ + public String format() { + String models = String.join(",", coreModels()); + int n = virtualCoreCount(); + return String.format("%s (%d %s)", models, n, (n == 1) ? "thread" : "threads"); + } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - CpuInfo cpuInfo = (CpuInfo) o; - return Objects.equals(coreInfos, cpuInfo.coreInfos); - } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CpuInfo cpuInfo = (CpuInfo) o; + return Objects.equals(coreInfos, cpuInfo.coreInfos); + } - @Override - public int hashCode() { - return Objects.hash(coreInfos); - } + @Override + public int hashCode() { + return Objects.hash(coreInfos); + } - @Override - public String toString() { - return "CpuInfo{" + - "coreInfos=" + coreInfos + - '}'; - } + @Override + public String toString() { + return "CpuInfo{" + "coreInfos=" + coreInfos + '}'; + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfo.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfo.java index 385eff8b2..c583cdc7f 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfo.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfo.java @@ -14,43 +14,43 @@ */ public class LinuxSystemInfo { - private static final Logger LOGGER = LoggerFactory.getLogger(LinuxSystemInfo.class); - - private final MemoryInfo memoryInfo; - private final CpuInfo cpuInfo; - - private LinuxSystemInfo(MemoryInfo memoryInfo, CpuInfo cpuInfo) { - this.memoryInfo = memoryInfo; - this.cpuInfo = cpuInfo; - } - - public MemoryInfo getMemoryInfo() { - return memoryInfo; - } - - public CpuInfo getCpuInfo() { - return cpuInfo; - } - - /** - * @return a snapshot of the current system state as a {@link LinuxSystemInfo} - */ - public static LinuxSystemInfo getCurrent() { - List meminfoLines = readFileOrEmpty(Path.of("/proc/meminfo")); - List cpuinfoLines = readFileOrEmpty(Path.of("/proc/cpuinfo")); - - MemoryInfo memoryInfo = MemoryInfo.fromMeminfo(meminfoLines); - CpuInfo cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(cpuinfoLines)); - - return new LinuxSystemInfo(memoryInfo, cpuInfo); - } - - private static List readFileOrEmpty(Path path) { - try { - return Files.readAllLines(path); - } catch (IOException e) { - LOGGER.warn("Failed to read system info from " + path.toAbsolutePath(), e); - return Collections.emptyList(); - } - } + private static final Logger LOGGER = LoggerFactory.getLogger(LinuxSystemInfo.class); + + private final MemoryInfo memoryInfo; + private final CpuInfo cpuInfo; + + private LinuxSystemInfo(MemoryInfo memoryInfo, CpuInfo cpuInfo) { + this.memoryInfo = memoryInfo; + this.cpuInfo = cpuInfo; + } + + public MemoryInfo getMemoryInfo() { + return memoryInfo; + } + + public CpuInfo getCpuInfo() { + return cpuInfo; + } + + /** + * @return a snapshot of the current system state as a {@link LinuxSystemInfo} + */ + public static LinuxSystemInfo getCurrent() { + List meminfoLines = readFileOrEmpty(Path.of("/proc/meminfo")); + List cpuinfoLines = readFileOrEmpty(Path.of("/proc/cpuinfo")); + + MemoryInfo memoryInfo = MemoryInfo.fromMeminfo(meminfoLines); + CpuInfo cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(cpuinfoLines)); + + return new LinuxSystemInfo(memoryInfo, cpuInfo); + } + + private static List readFileOrEmpty(Path path) { + try { + return Files.readAllLines(path); + } catch (IOException e) { + LOGGER.warn("Failed to read system info from " + path.toAbsolutePath(), e); + return Collections.emptyList(); + } + } } diff --git a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfo.java b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfo.java index b2025365a..e452e9737 100644 --- a/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfo.java +++ b/backend/shared/src/main/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfo.java @@ -3,93 +3,93 @@ import java.util.List; import java.util.Optional; -/** - * Contains information about system memory. - */ +/** Contains information about system memory. */ public class MemoryInfo { - private final long totalMemoryKib; - private final long freeMemoryKib; - private final long availableMemoryKib; + private final long totalMemoryKib; + private final long freeMemoryKib; + private final long availableMemoryKib; - public MemoryInfo(long totalMemoryKib, long freeMemoryKib, long availableMemoryKib) { - this.totalMemoryKib = totalMemoryKib; - this.freeMemoryKib = freeMemoryKib; - this.availableMemoryKib = availableMemoryKib; - } + public MemoryInfo(long totalMemoryKib, long freeMemoryKib, long availableMemoryKib) { + this.totalMemoryKib = totalMemoryKib; + this.freeMemoryKib = freeMemoryKib; + this.availableMemoryKib = availableMemoryKib; + } - /** - * @return the total memory available on the system, in KiB - */ - public long totalMemoryKib() { - return totalMemoryKib; - } + /** + * @return the total memory available on the system, in KiB + */ + public long totalMemoryKib() { + return totalMemoryKib; + } - /** - * @return the amount of memory that is currently unused, in KiB - */ - public long freeMemoryKiB() { - return freeMemoryKib; - } + /** + * @return the amount of memory that is currently unused, in KiB + */ + public long freeMemoryKiB() { + return freeMemoryKib; + } - /** - * @return an estimate of the amount of memory available for starting new applications without - * swapping, in KiB. See also this - * commit in the linux kernel. - */ - public long getAvailableMemoryKib() { - return availableMemoryKib; - } + /** + * @return an estimate of the amount of memory available for starting new applications without + * swapping, in KiB. See also this + * commit in the linux kernel. + */ + public long getAvailableMemoryKib() { + return availableMemoryKib; + } - /** - * Creates a {@link MemoryInfo} instance from the "/proc/meminfo" file. - *
- * If the meminfo string is not valid, dummy values of -1 will be inserted. - * - * @param meminfo the /proc/meminfo lines - * @return the created memory info. - */ - public static MemoryInfo fromMeminfo(List meminfo) { - long memTotal = lineFromMeminfo(meminfo, "MemTotal").orElse(-1L); - long memFree = lineFromMeminfo(meminfo, "MemFree").orElse(-1L); - long memAvailable = lineFromMeminfo(meminfo, "MemAvailable").orElse(-1L); + /** + * Creates a {@link MemoryInfo} instance from the "/proc/meminfo" file.
+ * If the meminfo string is not valid, dummy values of -1 will be inserted. + * + * @param meminfo the /proc/meminfo lines + * @return the created memory info. + */ + public static MemoryInfo fromMeminfo(List meminfo) { + long memTotal = lineFromMeminfo(meminfo, "MemTotal").orElse(-1L); + long memFree = lineFromMeminfo(meminfo, "MemFree").orElse(-1L); + long memAvailable = lineFromMeminfo(meminfo, "MemAvailable").orElse(-1L); - return new MemoryInfo(memTotal, memFree, memAvailable); - } + return new MemoryInfo(memTotal, memFree, memAvailable); + } - private static Optional lineFromMeminfo(List meminfo, String lineName) { - return meminfo.stream() - .filter(it -> it.startsWith(lineName)) - .findFirst() - .map(it -> it.replaceAll("[^0-9]", "")) - .flatMap(MemoryInfo::parseLong); - } + private static Optional lineFromMeminfo(List meminfo, String lineName) { + return meminfo.stream() + .filter(it -> it.startsWith(lineName)) + .findFirst() + .map(it -> it.replaceAll("[^0-9]", "")) + .flatMap(MemoryInfo::parseLong); + } - private static Optional parseLong(String input) { - try { - return Optional.of(Long.parseLong(input)); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } + private static Optional parseLong(String input) { + try { + return Optional.of(Long.parseLong(input)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } - /** - * @return a human-readable string summarizing this info - */ - public String format() { - if (totalMemoryKib > 256 * 1024) { - return String - .format("%d MiB total, %d MiB available", totalMemoryKib / 1024, availableMemoryKib / 1024); - } else { - return String.format("%d KiB total, %d KiB available", totalMemoryKib, availableMemoryKib); - } - } + /** + * @return a human-readable string summarizing this info + */ + public String format() { + if (totalMemoryKib > 256 * 1024) { + return String.format( + "%d MiB total, %d MiB available", totalMemoryKib / 1024, availableMemoryKib / 1024); + } else { + return String.format("%d KiB total, %d KiB available", totalMemoryKib, availableMemoryKib); + } + } - @Override - public String toString() { - return "MemoryInfo{" + - "freeMemoryKib=" + freeMemoryKib + - ", totalMemoryKib=" + totalMemoryKib + - '}'; - } + @Override + public String toString() { + return "MemoryInfo{" + + "freeMemoryKib=" + + freeMemoryKib + + ", totalMemoryKib=" + + totalMemoryKib + + '}'; + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandlerTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandlerTest.java index e5b1940f7..a1ac99280 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandlerTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/HeartbeatHandlerTest.java @@ -13,43 +13,42 @@ class HeartbeatHandlerTest { - private HeartbeatHandler heartbeatHandler; - private HeartbeatWebsocket heartbeatWebsocket; - - @BeforeEach - void setUp() { - heartbeatWebsocket = mock(HeartbeatWebsocket.class); - heartbeatHandler = new HeartbeatHandler(heartbeatWebsocket, 100); - - when(heartbeatWebsocket.sendPing()).thenReturn(true); - } - - @AfterEach - void tearDown() { - heartbeatHandler.shutdown(); - } - - - @Test - void testSendsPing() { - verify(heartbeatWebsocket, timeout(200).atLeastOnce()).onTimeoutDetected(); - } - - @Test - void onPongDelaysReaping() throws InterruptedException { - for (int i = 0; i < 20; i++) { - Thread.sleep(20); - heartbeatHandler.onPong(); - } - - verify(heartbeatWebsocket, never()).onTimeoutDetected(); - } - - @Test - void doesNotReapIfPingFails() throws InterruptedException { - when(heartbeatWebsocket.sendPing()).thenReturn(false); - Thread.sleep(200); - - verify(heartbeatWebsocket, never()).onTimeoutDetected(); - } + private HeartbeatHandler heartbeatHandler; + private HeartbeatWebsocket heartbeatWebsocket; + + @BeforeEach + void setUp() { + heartbeatWebsocket = mock(HeartbeatWebsocket.class); + heartbeatHandler = new HeartbeatHandler(heartbeatWebsocket, 100); + + when(heartbeatWebsocket.sendPing()).thenReturn(true); + } + + @AfterEach + void tearDown() { + heartbeatHandler.shutdown(); + } + + @Test + void testSendsPing() { + verify(heartbeatWebsocket, timeout(200).atLeastOnce()).onTimeoutDetected(); + } + + @Test + void onPongDelaysReaping() throws InterruptedException { + for (int i = 0; i < 20; i++) { + Thread.sleep(20); + heartbeatHandler.onPong(); + } + + verify(heartbeatWebsocket, never()).onTimeoutDetected(); + } + + @Test + void doesNotReapIfPingFails() throws InterruptedException { + when(heartbeatWebsocket.sendPing()).thenReturn(false); + Thread.sleep(200); + + verify(heartbeatWebsocket, never()).onTimeoutDetected(); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerBasedTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerBasedTest.java index f1a6deca9..5bcc12432 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerBasedTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerBasedTest.java @@ -3,24 +3,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; -/** - * Base class for tests that need a {@link Serializer}. - */ +/** Base class for tests that need a {@link Serializer}. */ public abstract class SerializerBasedTest { - /** - * Use this to serialize or deserialize your data. - */ - protected Serializer serializer; + /** Use this to serialize or deserialize your data. */ + protected Serializer serializer; - /** - * Use this to create {@link com.fasterxml.jackson.databind.JsonNode}s whenever necessary. - */ - protected ObjectMapper objectMapper; + /** Use this to create {@link com.fasterxml.jackson.databind.JsonNode}s whenever necessary. */ + protected ObjectMapper objectMapper; - @BeforeEach - void setUpObjectMapper() { - serializer = new Serializer(); - objectMapper = new ObjectMapper(); - } + @BeforeEach + void setUpObjectMapper() { + serializer = new Serializer(); + objectMapper = new ObjectMapper(); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerTest.java index 6d8aa762b..b565c60ad 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/SerializerTest.java @@ -6,8 +6,8 @@ public class SerializerTest extends SerializerBasedTest { - @Test - void deserializeFailReturnsEmpty() { - assertThat(serializer.deserialize("hello", SerializerBasedTest.class)).isEmpty(); - } + @Test + void deserializeFailReturnsEmpty() { + assertThat(serializer.deserialize("hello", SerializerBasedTest.class)).isEmpty(); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRunTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRunTest.java index b04b24e08..d7c8711ae 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRunTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/AbortRunTest.java @@ -10,16 +10,15 @@ class AbortRunTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new AbortRun()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new AbortRun()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", AbortRun.class); - assertTrue(result.isPresent()); - } - -} \ No newline at end of file + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", AbortRun.class); + assertTrue(result.isPresent()); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResultTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResultTest.java index 6d5072459..f6e0e25a9 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResultTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClearResultTest.java @@ -10,15 +10,15 @@ class ClearResultTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new ClearResult()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new ClearResult()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", ClearResult.class); - assertTrue(result.isPresent()); - } + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", ClearResult.class); + assertTrue(result.isPresent()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketTest.java index 5b48863dc..b3f351c55 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/ClientBoundPacketTest.java @@ -11,33 +11,27 @@ class ClientBoundPacketTest extends SerializerBasedTest { - @Test - void serializeCorrectly() throws JsonProcessingException { - ClientBoundPacket packet = new ClientBoundPacket( - ClientBoundPacketType.CLEAR_RESULT, - objectMapper.createArrayNode() - ); - JsonNode packetTree = serializer.serializeTree(packet); - - String expected = "{\"type\": \"clear_result\", \"data\": []}"; - JsonNode expectedTree = objectMapper.readTree(expected); - - assertEquals(expectedTree, packetTree); - } - - @Test - void deserializeCorrectly() { - String json = "{\"type\": \"request_run_reply\", \"data\": {}}"; - Optional result = serializer.deserialize(json, - ClientBoundPacket.class); - - assertTrue(result.isPresent()); - assertEquals( - new ClientBoundPacket( - ClientBoundPacketType.REQUEST_RUN_REPLY, - objectMapper.createObjectNode() - ), - result.get() - ); - } -} \ No newline at end of file + @Test + void serializeCorrectly() throws JsonProcessingException { + ClientBoundPacket packet = + new ClientBoundPacket(ClientBoundPacketType.CLEAR_RESULT, objectMapper.createArrayNode()); + JsonNode packetTree = serializer.serializeTree(packet); + + String expected = "{\"type\": \"clear_result\", \"data\": []}"; + JsonNode expectedTree = objectMapper.readTree(expected); + + assertEquals(expectedTree, packetTree); + } + + @Test + void deserializeCorrectly() { + String json = "{\"type\": \"request_run_reply\", \"data\": {}}"; + Optional result = serializer.deserialize(json, ClientBoundPacket.class); + + assertTrue(result.isPresent()); + assertEquals( + new ClientBoundPacket( + ClientBoundPacketType.REQUEST_RUN_REPLY, objectMapper.createObjectNode()), + result.get()); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResultTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResultTest.java index 5f54c404f..66e57839b 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResultTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetResultTest.java @@ -10,15 +10,15 @@ class GetResultTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new GetResult()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new GetResult()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", GetResult.class); - assertTrue(result.isPresent()); - } + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", GetResult.class); + assertTrue(result.isPresent()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatusTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatusTest.java index b6e388a56..eed574ec2 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatusTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/GetStatusTest.java @@ -10,15 +10,15 @@ class GetStatusTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new GetStatus()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new GetStatus()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", GetStatus.class); - assertTrue(result.isPresent()); - } + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", GetStatus.class); + assertTrue(result.isPresent()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReplyTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReplyTest.java index a84fa4fa2..1d26a280b 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReplyTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/clientbound/RequestRunReplyTest.java @@ -11,29 +11,23 @@ class RequestRunReplyTest extends SerializerBasedTest { - @Test - void deserializeWithNoOptionals() throws JsonProcessingException { - String json = "{\"bench\": false, \"run\": false}"; - Optional result = serializer.deserialize(json, RequestRunReply.class); - - assertTrue(result.isPresent()); - assertEquals( - new RequestRunReply(false, null, false, null), - result.get() - ); - } - - @Test - void deserializeWithAllOptionals() throws JsonProcessingException { - String json = "{\"bench\": true, \"bench_hash\": \"yay, I'm a hash\", \"run\": true, \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"}"; - Optional result = serializer.deserialize(json, RequestRunReply.class); - - UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); - assertTrue(result.isPresent()); - assertEquals( - new RequestRunReply(true, "yay, I'm a hash", true, uuid), - result.get() - ); - } - + @Test + void deserializeWithNoOptionals() throws JsonProcessingException { + String json = "{\"bench\": false, \"run\": false}"; + Optional result = serializer.deserialize(json, RequestRunReply.class); + + assertTrue(result.isPresent()); + assertEquals(new RequestRunReply(false, null, false, null), result.get()); + } + + @Test + void deserializeWithAllOptionals() throws JsonProcessingException { + String json = + "{\"bench\": true, \"bench_hash\": \"yay, I'm a hash\", \"run\": true, \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"}"; + Optional result = serializer.deserialize(json, RequestRunReply.class); + + UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); + assertTrue(result.isPresent()); + assertEquals(new RequestRunReply(true, "yay, I'm a hash", true, uuid), result.get()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReplyTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReplyTest.java index 605638ec1..214296ee4 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReplyTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/AbortRunReplyTest.java @@ -10,16 +10,15 @@ class AbortRunReplyTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new AbortRunReply()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new AbortRunReply()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", AbortRunReply.class); - assertTrue(result.isPresent()); - } - -} \ No newline at end of file + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", AbortRunReply.class); + assertTrue(result.isPresent()); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReplyTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReplyTest.java index ec8e46566..8259f85e9 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReplyTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ClearResultReplyTest.java @@ -10,15 +10,15 @@ class ClearResultReplyTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new ClearResultReply()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new ClearResultReply()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", ClearResultReply.class); - assertTrue(result.isPresent()); - } + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", ClearResultReply.class); + assertTrue(result.isPresent()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReplyTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReplyTest.java index cebef1a10..e8d8a4b25 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReplyTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetResultReplyTest.java @@ -16,145 +16,134 @@ class GetResultReplyTest extends SerializerBasedTest { - @Test - void deserializeRunnerError() { - Instant targetInstant = Instant.parse("2020-02-10T00:00:00Z"); - String string = "{" - + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," - + " \"success\": false," - + " \"error\": \"me not worky\"," - + " \"start_time\": \"2020-02-10T00:00:00Z\"," - + " \"stop_time\": \"2020-02-10T00:00:00Z\"" - + "}"; - Optional result = serializer.deserialize(string, GetResultReply.class); + @Test + void deserializeRunnerError() { + Instant targetInstant = Instant.parse("2020-02-10T00:00:00Z"); + String string = + "{" + + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," + + " \"success\": false," + + " \"error\": \"me not worky\"," + + " \"start_time\": \"2020-02-10T00:00:00Z\"," + + " \"stop_time\": \"2020-02-10T00:00:00Z\"" + + "}"; + Optional result = serializer.deserialize(string, GetResultReply.class); - UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); - assertTrue(result.isPresent()); - assertEquals( - new GetResultReply(uuid, false, null, "me not worky", targetInstant, targetInstant), - result.get() - ); - } + UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); + assertTrue(result.isPresent()); + assertEquals( + new GetResultReply(uuid, false, null, "me not worky", targetInstant, targetInstant), + result.get()); + } - @Test - void deserializeRunnerResultWithoutMetrics() { - Instant targetInstant = Instant.parse("2020-02-10T00:00:00Z"); - String string = "{" - + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," - + " \"success\": true," - + " \"result\": {\"error\": \"me also not worky\"}," - + " \"start_time\": \"2020-02-10T00:00:00Z\"," - + " \"stop_time\": \"2020-02-10T00:00:00Z\"" - + "}"; - Optional result = serializer.deserialize(string, GetResultReply.class); + @Test + void deserializeRunnerResultWithoutMetrics() { + Instant targetInstant = Instant.parse("2020-02-10T00:00:00Z"); + String string = + "{" + + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," + + " \"success\": true," + + " \"result\": {\"error\": \"me also not worky\"}," + + " \"start_time\": \"2020-02-10T00:00:00Z\"," + + " \"stop_time\": \"2020-02-10T00:00:00Z\"" + + "}"; + Optional result = serializer.deserialize(string, GetResultReply.class); - UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); - assertTrue(result.isPresent()); - assertEquals( - new GetResultReply( - uuid, - true, - new Result(null, "me also not worky"), - null, - targetInstant, - targetInstant - ), - result.get() - ); - } + UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); + assertTrue(result.isPresent()); + assertEquals( + new GetResultReply( + uuid, true, new Result(null, "me also not worky"), null, targetInstant, targetInstant), + result.get()); + } - @Test - void deserializeRunnerResultWithoutStopTime() { - String string = "{" - + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," - + " \"success\": true," - + " \"result\": {\"error\": \"me also not worky\"}," - + " \"start_time\": \"2020-02-10T00:00:00Z\"" - + "}"; - Optional result = serializer.deserialize(string, GetResultReply.class); - assertTrue(result.isEmpty()); - } + @Test + void deserializeRunnerResultWithoutStopTime() { + String string = + "{" + + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," + + " \"success\": true," + + " \"result\": {\"error\": \"me also not worky\"}," + + " \"start_time\": \"2020-02-10T00:00:00Z\"" + + "}"; + Optional result = serializer.deserialize(string, GetResultReply.class); + assertTrue(result.isEmpty()); + } - @Test - void deserializeRunnerResultWithoutStartTime() { - String string = "{" - + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," - + " \"success\": true," - + " \"result\": {\"error\": \"me also not worky\"}," - + " \"stop_time\": \"2020-02-10T00:00:00Z\"" - + "}"; - Optional result = serializer.deserialize(string, GetResultReply.class); - assertTrue(result.isEmpty()); - } + @Test + void deserializeRunnerResultWithoutStartTime() { + String string = + "{" + + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," + + " \"success\": true," + + " \"result\": {\"error\": \"me also not worky\"}," + + " \"stop_time\": \"2020-02-10T00:00:00Z\"" + + "}"; + Optional result = serializer.deserialize(string, GetResultReply.class); + assertTrue(result.isEmpty()); + } - @Test - void deserializeRunnerResultWithMetrics() { - Instant start = Instant.parse("2020-02-10T00:00:00Z"); - Instant stop = Instant.parse("2020-05-10T00:00:20Z"); - String string = "{" - + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," - + " \"success\": true," - + " \"result\": {" - + " \"benchmarks\": [" - + " {" - + " \"name\": \"sample benchmark\"," - + " \"metrics\": [" - + " {" - + " \"name\": \"sample metric\"," - + " \"unit\": \"barn\"," - + " \"interpretation\": \"MORE_IS_BETTER\"," - + " \"error\": \"something went wrong\"" - + " }," - + " {" - + " \"name\": \"other metric\"," - + " \"unit\": \"seconds\"," - + " \"interpretation\": \"LESS_IS_BETTER\"," - + " \"values\": [1, 2, 3]" - + " }" - + " ]" - + " }" - + " ]" - + " }," - + " \"start_time\": \"2020-02-10T00:00:00Z\"," - + " \"stop_time\": \"2020-05-10T00:00:20Z\"" - + "}"; - Optional result = serializer.deserialize(string, GetResultReply.class); + @Test + void deserializeRunnerResultWithMetrics() { + Instant start = Instant.parse("2020-02-10T00:00:00Z"); + Instant stop = Instant.parse("2020-05-10T00:00:20Z"); + String string = + "{" + + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," + + " \"success\": true," + + " \"result\": {" + + " \"benchmarks\": [" + + " {" + + " \"name\": \"sample benchmark\"," + + " \"metrics\": [" + + " {" + + " \"name\": \"sample metric\"," + + " \"unit\": \"barn\"," + + " \"interpretation\": \"MORE_IS_BETTER\"," + + " \"error\": \"something went wrong\"" + + " }," + + " {" + + " \"name\": \"other metric\"," + + " \"unit\": \"seconds\"," + + " \"interpretation\": \"LESS_IS_BETTER\"," + + " \"values\": [1, 2, 3]" + + " }" + + " ]" + + " }" + + " ]" + + " }," + + " \"start_time\": \"2020-02-10T00:00:00Z\"," + + " \"stop_time\": \"2020-05-10T00:00:20Z\"" + + "}"; + Optional result = serializer.deserialize(string, GetResultReply.class); - UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); - assertTrue(result.isPresent()); - assertEquals( - new GetResultReply( - uuid, - true, - new Result( - List.of( - new Benchmark( - "sample benchmark", - List.of( - new Metric( - "sample metric", - "something went wrong", - "barn", - Interpretation.MORE_IS_BETTER, - null - ), - new Metric( - "other metric", - null, - "seconds", - Interpretation.LESS_IS_BETTER, - List.of(1.0, 2.0, 3.0) - ) - ) - ) - ), - null - ), - null, - start, - stop - ), - result.get() - ); - } + UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); + assertTrue(result.isPresent()); + assertEquals( + new GetResultReply( + uuid, + true, + new Result( + List.of( + new Benchmark( + "sample benchmark", + List.of( + new Metric( + "sample metric", + "something went wrong", + "barn", + Interpretation.MORE_IS_BETTER, + null), + new Metric( + "other metric", + null, + "seconds", + Interpretation.LESS_IS_BETTER, + List.of(1.0, 2.0, 3.0))))), + null), + null, + start, + stop), + result.get()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReplyTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReplyTest.java index 59c985a39..3f26f7f5d 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReplyTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/GetStatusReplyTest.java @@ -13,54 +13,46 @@ class GetStatusReplyTest extends SerializerBasedTest { - @Test - void deserializeWithNoOptionals() { - String json = "{\"info\": \"system info goes here\", \"result_available\": false, \"status\": \"IDLE\"}"; - Optional result = serializer.deserialize(json, GetStatusReply.class); + @Test + void deserializeWithNoOptionals() { + String json = + "{\"info\": \"system info goes here\", \"result_available\": false, \"status\": \"IDLE\"}"; + Optional result = serializer.deserialize(json, GetStatusReply.class); - assertTrue(result.isPresent()); - assertEquals(new GetStatusReply( - "system info goes here", - null, - null, - false, - Status.IDLE, - null, - null - ), - result.get() - ); - } + assertTrue(result.isPresent()); + assertEquals( + new GetStatusReply("system info goes here", null, null, false, Status.IDLE, null, null), + result.get()); + } - @Test - void deserializeWithAllOptionals() { - String json = "{" - + "\"info\": \"system info goes here\"," - + " \"version_hash\": \"bloop\"," - + " \"bench_hash\": \"blabla\"," - + " \"result_available\": false," - + " \"status\": \"ABORT\"," - + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," - + " \"last_output_lines\": {" - + " \"first_line_offset\": 20," - + " \"lines\": [\"this is a line\",\"With a newline\"]" - + " }" - + "}"; - Optional result = serializer.deserialize(json, GetStatusReply.class); + @Test + void deserializeWithAllOptionals() { + String json = + "{" + + "\"info\": \"system info goes here\"," + + " \"version_hash\": \"bloop\"," + + " \"bench_hash\": \"blabla\"," + + " \"result_available\": false," + + " \"status\": \"ABORT\"," + + " \"run_id\": \"576afdcb-eaf9-46b2-9287-fc3bf8df83df\"," + + " \"last_output_lines\": {" + + " \"first_line_offset\": 20," + + " \"lines\": [\"this is a line\",\"With a newline\"]" + + " }" + + "}"; + Optional result = serializer.deserialize(json, GetStatusReply.class); - UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); - assertTrue(result.isPresent()); - assertEquals( - new GetStatusReply( - "system info goes here", - "bloop", - "blabla", - false, - Status.ABORT, - uuid, - new LinesWithOffset(20, List.of("this is a line", "With a newline")) - ), - result.get() - ); - } + UUID uuid = UUID.fromString("576afdcb-eaf9-46b2-9287-fc3bf8df83df"); + assertTrue(result.isPresent()); + assertEquals( + new GetStatusReply( + "system info goes here", + "bloop", + "blabla", + false, + Status.ABORT, + uuid, + new LinesWithOffset(20, List.of("this is a line", "With a newline"))), + result.get()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRunTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRunTest.java index 411a19a0d..65d171d8b 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRunTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/RequestRunTest.java @@ -10,15 +10,15 @@ class RequestRunTest extends SerializerBasedTest { - @Test - void serializeToEmptyObject() { - JsonNode tree = serializer.serializeTree(new RequestRun()); - assertEquals(objectMapper.createObjectNode(), tree); - } + @Test + void serializeToEmptyObject() { + JsonNode tree = serializer.serializeTree(new RequestRun()); + assertEquals(objectMapper.createObjectNode(), tree); + } - @Test - void deserializeFromEmptyObject() { - Optional result = serializer.deserialize("{}", RequestRun.class); - assertTrue(result.isPresent()); - } + @Test + void deserializeFromEmptyObject() { + Optional result = serializer.deserialize("{}", RequestRun.class); + assertTrue(result.isPresent()); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketTest.java index f4abebdf5..16979b69f 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/serialization/serverbound/ServerBoundPacketTest.java @@ -11,33 +11,27 @@ class ServerBoundPacketTest extends SerializerBasedTest { - @Test - void serializeCorrectly() throws JsonProcessingException { - ServerBoundPacket packet = new ServerBoundPacket( - ServerBoundPacketType.REQUEST_RUN, - objectMapper.createArrayNode() - ); - JsonNode packetTree = serializer.serializeTree(packet); - - String expected = "{\"type\": \"request_run\", \"data\": []}"; - JsonNode expectedTree = objectMapper.readTree(expected); - - assertEquals(expectedTree, packetTree); - } - - @Test - void deserializeCorrectly() throws JsonProcessingException { - String json = "{\"type\": \"get_status_reply\", \"data\": {}}"; - Optional result = serializer.deserialize(json, - ServerBoundPacket.class); - - assertTrue(result.isPresent()); - assertEquals( - new ServerBoundPacket( - ServerBoundPacketType.GET_STATUS_REPLY, - objectMapper.createObjectNode() - ), - result.get() - ); - } -} \ No newline at end of file + @Test + void serializeCorrectly() throws JsonProcessingException { + ServerBoundPacket packet = + new ServerBoundPacket(ServerBoundPacketType.REQUEST_RUN, objectMapper.createArrayNode()); + JsonNode packetTree = serializer.serializeTree(packet); + + String expected = "{\"type\": \"request_run\", \"data\": []}"; + JsonNode expectedTree = objectMapper.readTree(expected); + + assertEquals(expectedTree, packetTree); + } + + @Test + void deserializeCorrectly() throws JsonProcessingException { + String json = "{\"type\": \"get_status_reply\", \"data\": {}}"; + Optional result = serializer.deserialize(json, ServerBoundPacket.class); + + assertTrue(result.isPresent()); + assertEquals( + new ServerBoundPacket( + ServerBoundPacketType.GET_STATUS_REPLY, objectMapper.createObjectNode()), + result.get()); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachineTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachineTest.java index c478ec9f9..96d82d599 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachineTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/protocol/statemachine/StateMachineTest.java @@ -8,101 +8,105 @@ class StateMachineTest { - static class Resting implements State { - - @Override - public boolean isResting() { - return true; - } - } - - static class Unrest implements State { - - } - - static class RandomUnrest implements State { - - } - - private StateMachine stateMachine; - - @BeforeEach - void setUp() { - stateMachine = new StateMachine<>(new Resting()); - } - - @Test - void canChangeFromResting() throws InterruptedException { - assertThat(stateMachine.switchFromRestingState(new Unrest())).isTrue(); - } - - @Test - void canNotChangeOutOfUnrest() throws InterruptedException { - stateMachine.switchFromRestingState(new Unrest()); - - new Thread(() -> { - try { - Thread.sleep(1000); - stateMachine.changeCurrentState(state -> { - assertThat(state).isInstanceOf(Unrest.class); - return new Resting(); - }); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }).start(); - - assertThat(stateMachine.switchFromRestingState(new Unrest())).isTrue(); - } - - @Test - void changingFromStopFails() throws InterruptedException { - stateMachine.stop(); - assertThat(stateMachine.switchFromRestingState(new Unrest())).isFalse(); - } - - @Test - void changesState() throws InterruptedException { - Unrest unrest = new Unrest(); - RandomUnrest randomUnrest = new RandomUnrest(); - Resting resting = new Resting(); - - assertThat(stateMachine.switchFromRestingState(unrest)).isTrue(); - - stateMachine.changeCurrentState(old -> { - assertThat(old).isSameAs(unrest); - return randomUnrest; - }); - - stateMachine.changeCurrentState(old -> { - assertThat(old).isSameAs(randomUnrest); - return resting; - }); - - stateMachine.changeCurrentState(old -> { - assertThat(old).isSameAs(resting); - return resting; - }); - } - - @Test - void stopCancelsWait() throws InterruptedException { - stateMachine.switchFromRestingState(new Unrest()); - - Semaphore waiting = new Semaphore(0); - - new Thread(() -> { - try { - waiting.acquire(); - Thread.sleep(500); - stateMachine.stop(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }).start(); - - waiting.release(); - // The switch will release then the thread stops the state machine - assertThat(stateMachine.switchFromRestingState(new RandomUnrest())).isFalse(); - } + static class Resting implements State { + + @Override + public boolean isResting() { + return true; + } + } + + static class Unrest implements State {} + + static class RandomUnrest implements State {} + + private StateMachine stateMachine; + + @BeforeEach + void setUp() { + stateMachine = new StateMachine<>(new Resting()); + } + + @Test + void canChangeFromResting() throws InterruptedException { + assertThat(stateMachine.switchFromRestingState(new Unrest())).isTrue(); + } + + @Test + void canNotChangeOutOfUnrest() throws InterruptedException { + stateMachine.switchFromRestingState(new Unrest()); + + new Thread( + () -> { + try { + Thread.sleep(1000); + stateMachine.changeCurrentState( + state -> { + assertThat(state).isInstanceOf(Unrest.class); + return new Resting(); + }); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }) + .start(); + + assertThat(stateMachine.switchFromRestingState(new Unrest())).isTrue(); + } + + @Test + void changingFromStopFails() throws InterruptedException { + stateMachine.stop(); + assertThat(stateMachine.switchFromRestingState(new Unrest())).isFalse(); + } + + @Test + void changesState() throws InterruptedException { + Unrest unrest = new Unrest(); + RandomUnrest randomUnrest = new RandomUnrest(); + Resting resting = new Resting(); + + assertThat(stateMachine.switchFromRestingState(unrest)).isTrue(); + + stateMachine.changeCurrentState( + old -> { + assertThat(old).isSameAs(unrest); + return randomUnrest; + }); + + stateMachine.changeCurrentState( + old -> { + assertThat(old).isSameAs(randomUnrest); + return resting; + }); + + stateMachine.changeCurrentState( + old -> { + assertThat(old).isSameAs(resting); + return resting; + }); + } + + @Test + void stopCancelsWait() throws InterruptedException { + stateMachine.switchFromRestingState(new Unrest()); + + Semaphore waiting = new Semaphore(0); + + new Thread( + () -> { + try { + waiting.acquire(); + Thread.sleep(500); + stateMachine.stop(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }) + .start(); + + waiting.release(); + // The switch will release then the thread stops the state machine + assertThat(stateMachine.switchFromRestingState(new RandomUnrest())).isFalse(); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/EitherTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/EitherTest.java index 736d75934..ac34c7be9 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/EitherTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/EitherTest.java @@ -11,88 +11,82 @@ class EitherTest { - @Test - void testRight() { - Either either = Either.ofRight(true); - assertTrue(either.isRight()); - assertFalse(either.isLeft()); - assertEquals(Optional.of(true), either.getRight()); - assertEquals(Optional.empty(), either.getLeft()); - } - - @Test - void testLeft() { - Either either = Either.ofLeft(33); - assertFalse(either.isRight()); - assertTrue(either.isLeft()); - assertEquals(Optional.empty(), either.getRight()); - assertEquals(Optional.of(33), either.getLeft()); - } - - @Test - void testEquals() { - Either left = Either.ofLeft(24); - Either right = Either.ofRight(false); - Either weird = Either.ofRight(24); - - assertEquals(Either.ofLeft(24), left); - assertNotEquals(Either.ofLeft(23), left); - - assertEquals(Either.ofRight(false), right); - assertNotEquals(Either.ofRight(true), right); - - assertEquals(Either.ofRight(24), weird); - assertNotEquals(Either.ofRight(12), weird); - - assertNotEquals(left, right); - assertNotEquals(left, weird); - assertNotEquals(right, weird); - } - - @Test - void mapLeft() { - var mappedLeft = Either.ofLeft(20).mapLeft(it -> it + 40); - assertThat(mappedLeft.isLeft()).isTrue(); - assertThat(mappedLeft.getLeft()).isPresent(); - assertThat(mappedLeft.getRight()).isEmpty(); - assertThat(mappedLeft.getLeft()).isPresent().contains(60); - - var mappedRight = Either.ofRight(20).mapLeft(it -> "d"); - assertThat(mappedRight.isRight()).isTrue(); - assertThat(mappedRight.getLeft()).isEmpty(); - assertThat(mappedRight.getRight()).isPresent().contains(20); - } - - @Test - void mapRight() { - var mappedRight = Either.ofRight(20).mapRight(it -> it + 40); - assertThat(mappedRight.isRight()).isTrue(); - assertThat(mappedRight.getRight()).isPresent(); - assertThat(mappedRight.getLeft()).isEmpty(); - assertThat(mappedRight.getRight()).isPresent().contains(60); - - var mappedLeft = Either.ofLeft(20).mapRight(it -> "d"); - assertThat(mappedLeft.isLeft()).isTrue(); - assertThat(mappedLeft.getRight()).isEmpty(); - assertThat(mappedLeft.getLeft()).isPresent().contains(20); - } - - @Test - void mapBoth() { - Either lefty = Either.ofLeft(20).mapBoth( - i -> i + 40, - s -> s + "hey" - ); - - assertThat(lefty.getRight()).isEmpty(); - assertThat(lefty.getLeft()).isPresent().contains(60); - - var righty = Either.ofRight("you ").mapBoth( - i -> i + 40, - s -> s + "hey" - ); - - assertThat(righty.getLeft()).isEmpty(); - assertThat(righty.getRight()).isPresent().contains("you hey"); - } + @Test + void testRight() { + Either either = Either.ofRight(true); + assertTrue(either.isRight()); + assertFalse(either.isLeft()); + assertEquals(Optional.of(true), either.getRight()); + assertEquals(Optional.empty(), either.getLeft()); + } + + @Test + void testLeft() { + Either either = Either.ofLeft(33); + assertFalse(either.isRight()); + assertTrue(either.isLeft()); + assertEquals(Optional.empty(), either.getRight()); + assertEquals(Optional.of(33), either.getLeft()); + } + + @Test + void testEquals() { + Either left = Either.ofLeft(24); + Either right = Either.ofRight(false); + Either weird = Either.ofRight(24); + + assertEquals(Either.ofLeft(24), left); + assertNotEquals(Either.ofLeft(23), left); + + assertEquals(Either.ofRight(false), right); + assertNotEquals(Either.ofRight(true), right); + + assertEquals(Either.ofRight(24), weird); + assertNotEquals(Either.ofRight(12), weird); + + assertNotEquals(left, right); + assertNotEquals(left, weird); + assertNotEquals(right, weird); + } + + @Test + void mapLeft() { + var mappedLeft = Either.ofLeft(20).mapLeft(it -> it + 40); + assertThat(mappedLeft.isLeft()).isTrue(); + assertThat(mappedLeft.getLeft()).isPresent(); + assertThat(mappedLeft.getRight()).isEmpty(); + assertThat(mappedLeft.getLeft()).isPresent().contains(60); + + var mappedRight = Either.ofRight(20).mapLeft(it -> "d"); + assertThat(mappedRight.isRight()).isTrue(); + assertThat(mappedRight.getLeft()).isEmpty(); + assertThat(mappedRight.getRight()).isPresent().contains(20); + } + + @Test + void mapRight() { + var mappedRight = Either.ofRight(20).mapRight(it -> it + 40); + assertThat(mappedRight.isRight()).isTrue(); + assertThat(mappedRight.getRight()).isPresent(); + assertThat(mappedRight.getLeft()).isEmpty(); + assertThat(mappedRight.getRight()).isPresent().contains(60); + + var mappedLeft = Either.ofLeft(20).mapRight(it -> "d"); + assertThat(mappedLeft.isLeft()).isTrue(); + assertThat(mappedLeft.getRight()).isEmpty(); + assertThat(mappedLeft.getLeft()).isPresent().contains(20); + } + + @Test + void mapBoth() { + Either lefty = Either.ofLeft(20).mapBoth(i -> i + 40, s -> s + "hey"); + + assertThat(lefty.getRight()).isEmpty(); + assertThat(lefty.getLeft()).isPresent().contains(60); + + var righty = Either.ofRight("you ").mapBoth(i -> i + 40, s -> s + "hey"); + + assertThat(righty.getLeft()).isEmpty(); + assertThat(righty.getRight()).isPresent().contains("you hey"); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/ExceptionHelperTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/ExceptionHelperTest.java index 49a3a0ebc..e94e06ef1 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/ExceptionHelperTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/ExceptionHelperTest.java @@ -6,36 +6,34 @@ class ExceptionHelperTest { - @Test - void containsThisClass() { - String stackTrace = ExceptionHelper.getStackTrace(new Throwable()); - assertThat(stackTrace) - .contains("ExceptionHelperTest") - .contains("containsThisClass"); - } - - @Test - void containsNestedStacktrace() { - RuntimeException inner = new RuntimeException("hello there"); - RuntimeException outer = new RuntimeException(inner); - - String stackTrace = ExceptionHelper.getStackTrace(outer); - assertThat(stackTrace) - .contains("ExceptionHelperTest") - .contains("containsNestedStacktrace") - .contains("hello there"); - } - - @Test - void containsSuppressedStacktrace() { - RuntimeException inner = new RuntimeException("hello there"); - RuntimeException outer = new RuntimeException(); - outer.addSuppressed(inner); - - String stackTrace = ExceptionHelper.getStackTrace(outer); - assertThat(stackTrace) - .contains("ExceptionHelperTest") - .contains("containsSuppressedStacktrace") - .contains("hello there"); - } -} \ No newline at end of file + @Test + void containsThisClass() { + String stackTrace = ExceptionHelper.getStackTrace(new Throwable()); + assertThat(stackTrace).contains("ExceptionHelperTest").contains("containsThisClass"); + } + + @Test + void containsNestedStacktrace() { + RuntimeException inner = new RuntimeException("hello there"); + RuntimeException outer = new RuntimeException(inner); + + String stackTrace = ExceptionHelper.getStackTrace(outer); + assertThat(stackTrace) + .contains("ExceptionHelperTest") + .contains("containsNestedStacktrace") + .contains("hello there"); + } + + @Test + void containsSuppressedStacktrace() { + RuntimeException inner = new RuntimeException("hello there"); + RuntimeException outer = new RuntimeException(); + outer.addSuppressed(inner); + + String stackTrace = ExceptionHelper.getStackTrace(outer); + assertThat(stackTrace) + .contains("ExceptionHelperTest") + .contains("containsSuppressedStacktrace") + .contains("hello there"); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/FileHelperTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/FileHelperTest.java index 2a93efa7b..e9ec031df 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/FileHelperTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/FileHelperTest.java @@ -11,134 +11,116 @@ class FileHelperTest { - @TempDir - Path tempFolder; - - @Test - void canDeleteEmptyDir() throws IOException { - Path dirPath = tempFolder.resolve("test"); - Files.createDirectory(dirPath); - - FileHelper.deleteDirectoryOrFile(dirPath); - - assertFalse( - Files.exists(dirPath), "Dir not deleted" - ); - } - - @Test - void canDeleteDirWithFiles() throws IOException { - Path dirPath = tempFolder.resolve("test"); - Files.createDirectory(dirPath); - - for (int i = 0; i < 10; i++) { - Files.writeString(dirPath.resolve("hey" + i), "Test " + i); - } - - FileHelper.deleteDirectoryOrFile(dirPath); - - assertFalse( - Files.exists(dirPath), "Dir not deleted" - ); - } - - @Test - void canDeleteDirWithReadOnlyFiles() throws IOException { - Path dirPath = tempFolder.resolve("test"); - Files.createDirectory(dirPath); - - for (int i = 0; i < 10; i++) { - Path file = dirPath.resolve("hey" + i); - Files.writeString(file, "Test " + i); - file.toFile().setWritable(false); - } - - FileHelper.deleteDirectoryOrFile(dirPath); - - assertFalse( - Files.exists(dirPath), "Dir not deleted" - ); - } - - @Test - void canDeleteDirWithNestedDir() throws IOException { - Path dirPath = tempFolder.resolve("test"); - Files.createDirectory(dirPath); - - for (int i = 0; i < 10; i++) { - Path subDir = dirPath.resolve("hey" + i); - Files.createDirectory(subDir); - Files.writeString(subDir.resolve("hey" + i), "Test " + i); - } - - FileHelper.deleteDirectoryOrFile(dirPath); - - assertFalse( - Files.exists(dirPath), "Dir not deleted" - ); - } - - @Test - void deleteFile() throws IOException { - Path path = Files.createTempFile("test", "suff"); - try { - FileHelper.deleteDirectoryOrFile(path); - assertThat(Files.notExists(path)) - .withFailMessage("Did not delete file!") - .isTrue(); - } finally { - Files.deleteIfExists(path); - } - } - - @Test - void deleteEmptyDirectory() throws IOException { - Path path = Files.createTempDirectory("test"); - try { - FileHelper.deleteDirectoryOrFile(path); - assertThat(Files.notExists(path)) - .withFailMessage("Did not delete directory!") - .isTrue(); - } finally { - Files.deleteIfExists(path); - } - } - - @Test - void deleteNotEmptyDirectory() throws IOException { - Path path = Files.createTempDirectory("test"); - Path innerFile = Files.createFile(path.resolve("hey")); - try { - FileHelper.deleteDirectoryOrFile(path); - assertThat(Files.notExists(path)) - .withFailMessage("Did not delete directory!") - .isTrue(); - } finally { - Files.deleteIfExists(innerFile); - Files.deleteIfExists(path); - } - } - - @Test - void deleteNotExistingFile() throws IOException { - Path path = Files.createTempFile("test", "hey"); - Files.deleteIfExists(path); - FileHelper.deleteDirectoryOrFile(path); - } - - @Test - void uncheckedDeleteNotEmptyDirectory() throws IOException { - Path path = Files.createTempDirectory("test"); - Path innerFile = Files.createFile(path.resolve("hey")); - try { - FileHelper.uncheckedDeleteDirectoryOrFile(path); - assertThat(Files.notExists(path)) - .withFailMessage("Did not delete directory!") - .isTrue(); - } finally { - Files.deleteIfExists(innerFile); - Files.deleteIfExists(path); - } - } - -} \ No newline at end of file + @TempDir Path tempFolder; + + @Test + void canDeleteEmptyDir() throws IOException { + Path dirPath = tempFolder.resolve("test"); + Files.createDirectory(dirPath); + + FileHelper.deleteDirectoryOrFile(dirPath); + + assertFalse(Files.exists(dirPath), "Dir not deleted"); + } + + @Test + void canDeleteDirWithFiles() throws IOException { + Path dirPath = tempFolder.resolve("test"); + Files.createDirectory(dirPath); + + for (int i = 0; i < 10; i++) { + Files.writeString(dirPath.resolve("hey" + i), "Test " + i); + } + + FileHelper.deleteDirectoryOrFile(dirPath); + + assertFalse(Files.exists(dirPath), "Dir not deleted"); + } + + @Test + void canDeleteDirWithReadOnlyFiles() throws IOException { + Path dirPath = tempFolder.resolve("test"); + Files.createDirectory(dirPath); + + for (int i = 0; i < 10; i++) { + Path file = dirPath.resolve("hey" + i); + Files.writeString(file, "Test " + i); + file.toFile().setWritable(false); + } + + FileHelper.deleteDirectoryOrFile(dirPath); + + assertFalse(Files.exists(dirPath), "Dir not deleted"); + } + + @Test + void canDeleteDirWithNestedDir() throws IOException { + Path dirPath = tempFolder.resolve("test"); + Files.createDirectory(dirPath); + + for (int i = 0; i < 10; i++) { + Path subDir = dirPath.resolve("hey" + i); + Files.createDirectory(subDir); + Files.writeString(subDir.resolve("hey" + i), "Test " + i); + } + + FileHelper.deleteDirectoryOrFile(dirPath); + + assertFalse(Files.exists(dirPath), "Dir not deleted"); + } + + @Test + void deleteFile() throws IOException { + Path path = Files.createTempFile("test", "suff"); + try { + FileHelper.deleteDirectoryOrFile(path); + assertThat(Files.notExists(path)).withFailMessage("Did not delete file!").isTrue(); + } finally { + Files.deleteIfExists(path); + } + } + + @Test + void deleteEmptyDirectory() throws IOException { + Path path = Files.createTempDirectory("test"); + try { + FileHelper.deleteDirectoryOrFile(path); + assertThat(Files.notExists(path)).withFailMessage("Did not delete directory!").isTrue(); + } finally { + Files.deleteIfExists(path); + } + } + + @Test + void deleteNotEmptyDirectory() throws IOException { + Path path = Files.createTempDirectory("test"); + Path innerFile = Files.createFile(path.resolve("hey")); + try { + FileHelper.deleteDirectoryOrFile(path); + assertThat(Files.notExists(path)).withFailMessage("Did not delete directory!").isTrue(); + } finally { + Files.deleteIfExists(innerFile); + Files.deleteIfExists(path); + } + } + + @Test + void deleteNotExistingFile() throws IOException { + Path path = Files.createTempFile("test", "hey"); + Files.deleteIfExists(path); + FileHelper.deleteDirectoryOrFile(path); + } + + @Test + void uncheckedDeleteNotEmptyDirectory() throws IOException { + Path path = Files.createTempDirectory("test"); + Path innerFile = Files.createFile(path.resolve("hey")); + try { + FileHelper.uncheckedDeleteDirectoryOrFile(path); + assertThat(Files.notExists(path)).withFailMessage("Did not delete directory!").isTrue(); + } finally { + Files.deleteIfExists(innerFile); + Files.deleteIfExists(path); + } + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/OSCheckTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/OSCheckTest.java index c22dbea13..5dcd8b10c 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/OSCheckTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/OSCheckTest.java @@ -6,12 +6,12 @@ class OSCheckTest { - @Test - void returnsCorrectValue() { - if (System.getProperty("os.name").toLowerCase().contains("windows")) { - assertThat(OSCheck.isStupidWindows()).isTrue(); - } else { - assertThat(OSCheck.isStupidWindows()).isFalse(); - } - } -} \ No newline at end of file + @Test + void returnsCorrectValue() { + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + assertThat(OSCheck.isStupidWindows()).isTrue(); + } else { + assertThat(OSCheck.isStupidWindows()).isFalse(); + } + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/PairTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/PairTest.java index 52386c3b6..9c8cd18e6 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/PairTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/PairTest.java @@ -7,15 +7,14 @@ class PairTest { - @Test - void testContents() { - Pair pair = new Pair<>(33, true); - assertEquals(33, pair.getFirst()); - assertEquals(true, pair.getSecond()); - - assertEquals(new Pair<>(33, true), pair); - assertNotEquals(new Pair<>(33, false), pair); - assertNotEquals(new Pair<>(34, true), pair); - } + @Test + void testContents() { + Pair pair = new Pair<>(33, true); + assertEquals(33, pair.getFirst()); + assertEquals(true, pair.getSecond()); + assertEquals(new Pair<>(33, true), pair); + assertNotEquals(new Pair<>(33, false), pair); + assertNotEquals(new Pair<>(34, true), pair); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/StringOutputStreamTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/StringOutputStreamTest.java index 8ba79ea60..8fac20c5b 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/StringOutputStreamTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/StringOutputStreamTest.java @@ -10,62 +10,46 @@ class StringOutputStreamTest { - private StringOutputStream outputStream; - - @BeforeEach - void setUp() { - outputStream = new StringOutputStream(); - } - - @Test - void writeSingleChar() { - char character = (char) ('A' + ThreadLocalRandom.current().nextInt('z' - 'A')); - outputStream.write(character); - assertEquals( - character + "", - outputStream.getString() - ); - } - - @Test - void writeString() throws IOException { - String text = "Hello world there"; - outputStream.write(text.getBytes()); - assertEquals( - text, - outputStream.getString() - ); - } - - @Test - void writeStringMultipleCalls() throws IOException { - String text = "Hello world there"; - outputStream.write(text.substring(0, 10).getBytes()); - outputStream.write(text.substring(10).getBytes()); - assertEquals( - text, - outputStream.getString() - ); - } - - @Test - void writeIsUTF8() throws IOException { - String text = "Hello world thereäöüß Λ놇"; - outputStream.write(text.getBytes(StandardCharsets.UTF_8)); - assertEquals( - text, - outputStream.getString() - ); - } - - @Test - void writePartial() { - String text = "Hello world"; - outputStream.write(text.getBytes(StandardCharsets.UTF_8), 0, 6); - assertEquals( - text.substring(0, 6), - outputStream.getString() - ); - } - -} \ No newline at end of file + private StringOutputStream outputStream; + + @BeforeEach + void setUp() { + outputStream = new StringOutputStream(); + } + + @Test + void writeSingleChar() { + char character = (char) ('A' + ThreadLocalRandom.current().nextInt('z' - 'A')); + outputStream.write(character); + assertEquals(character + "", outputStream.getString()); + } + + @Test + void writeString() throws IOException { + String text = "Hello world there"; + outputStream.write(text.getBytes()); + assertEquals(text, outputStream.getString()); + } + + @Test + void writeStringMultipleCalls() throws IOException { + String text = "Hello world there"; + outputStream.write(text.substring(0, 10).getBytes()); + outputStream.write(text.substring(10).getBytes()); + assertEquals(text, outputStream.getString()); + } + + @Test + void writeIsUTF8() throws IOException { + String text = "Hello world thereäöüß Λ놇"; + outputStream.write(text.getBytes(StandardCharsets.UTF_8)); + assertEquals(text, outputStream.getString()); + } + + @Test + void writePartial() { + String text = "Hello world"; + outputStream.write(text.getBytes(StandardCharsets.UTF_8), 0, 6); + assertEquals(text.substring(0, 6), outputStream.getString()); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/TimeoutTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/TimeoutTest.java index 5b7c51d71..99cf6efc7 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/TimeoutTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/TimeoutTest.java @@ -9,56 +9,56 @@ class TimeoutTest { - private static final Duration TIMEOUT_DURATION = Duration.ofMillis(100); + private static final Duration TIMEOUT_DURATION = Duration.ofMillis(100); - private volatile boolean completed; + private volatile boolean completed; - @BeforeEach - void setUp() { - completed = false; - } + @BeforeEach + void setUp() { + completed = false; + } - private void handler(Void aVoid) { - completed = true; - } + private void handler(Void aVoid) { + completed = true; + } - @Test - void timeOutProperly() throws InterruptedException { - Timeout timeout = Timeout.after(TIMEOUT_DURATION); - timeout.getCompletionStage().thenAccept(this::handler); - timeout.start(); + @Test + void timeOutProperly() throws InterruptedException { + Timeout timeout = Timeout.after(TIMEOUT_DURATION); + timeout.getCompletionStage().thenAccept(this::handler); + timeout.start(); - Thread.sleep(TIMEOUT_DURATION.toMillis() * 2); - assertTrue(completed); - } + Thread.sleep(TIMEOUT_DURATION.toMillis() * 2); + assertTrue(completed); + } - @Test - void cancelWhileRunning() throws InterruptedException { - Timeout timeout = Timeout.after(TIMEOUT_DURATION); - timeout.getCompletionStage().thenAccept(this::handler); - timeout.start(); + @Test + void cancelWhileRunning() throws InterruptedException { + Timeout timeout = Timeout.after(TIMEOUT_DURATION); + timeout.getCompletionStage().thenAccept(this::handler); + timeout.start(); - Thread.sleep(TIMEOUT_DURATION.toMillis() / 2); - timeout.cancel(); + Thread.sleep(TIMEOUT_DURATION.toMillis() / 2); + timeout.cancel(); - Thread.sleep(TIMEOUT_DURATION.toMillis() * 2); - assertFalse(completed); - } + Thread.sleep(TIMEOUT_DURATION.toMillis() * 2); + assertFalse(completed); + } - @Test - void cancelMultipleTimes() throws InterruptedException { - Timeout timeout = Timeout.after(TIMEOUT_DURATION); - timeout.getCompletionStage().thenAccept(this::handler); - timeout.start(); + @Test + void cancelMultipleTimes() throws InterruptedException { + Timeout timeout = Timeout.after(TIMEOUT_DURATION); + timeout.getCompletionStage().thenAccept(this::handler); + timeout.start(); - Thread.sleep(TIMEOUT_DURATION.toMillis() / 2); - timeout.cancel(); - timeout.cancel(); - timeout.cancel(); - timeout.cancel(); - timeout.cancel(); + Thread.sleep(TIMEOUT_DURATION.toMillis() / 2); + timeout.cancel(); + timeout.cancel(); + timeout.cancel(); + timeout.cancel(); + timeout.cancel(); - Thread.sleep(TIMEOUT_DURATION.toMillis() * 2); - assertFalse(completed); - } + Thread.sleep(TIMEOUT_DURATION.toMillis() * 2); + assertFalse(completed); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelperTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelperTest.java index 9b3db73d5..86424b016 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelperTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/PermissionsHelperTest.java @@ -10,25 +10,19 @@ class PermissionsHelperTest { - @ParameterizedTest(name = "Mode {0} maps to \"{1}\"") - @CsvSource({ - "0100, --x------", - "0111, --x--x--x", - "0444, r--r--r--", - "0222, -w--w--w-", - "0421, r---w---x", - "0461, r--rw---x", - "0467, r--rw-rwx", - }) - void fewBaseTests(int mode, String permissionString) { - Set permissions = PosixFilePermissions.fromString(permissionString); - assertEquals( - permissions, - PermissionsHelper.fromOctal(mode) - ); - assertEquals( - mode, - PermissionsHelper.toOctal(permissions) - ); - } -} \ No newline at end of file + @ParameterizedTest(name = "Mode {0} maps to \"{1}\"") + @CsvSource({ + "0100, --x------", + "0111, --x--x--x", + "0444, r--r--r--", + "0222, -w--w--w-", + "0421, r---w---x", + "0461, r--rw---x", + "0467, r--rw-rwx", + }) + void fewBaseTests(int mode, String permissionString) { + Set permissions = PosixFilePermissions.fromString(permissionString); + assertEquals(permissions, PermissionsHelper.fromOctal(mode)); + assertEquals(mode, PermissionsHelper.toOctal(permissions)); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/TarHelperTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/TarHelperTest.java index 85e34460b..07d2f48c4 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/TarHelperTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/compression/TarHelperTest.java @@ -21,93 +21,85 @@ class TarHelperTest { - @TempDir - Path rootDir; - Path resultDir; - Path tempDir; + @TempDir Path rootDir; + Path resultDir; + Path tempDir; - @BeforeEach - void setUp() throws IOException { - resultDir = rootDir.resolve("res"); - tempDir = rootDir.resolve("temp"); - Files.createDirectory(tempDir); - Files.createDirectory(resultDir); - } + @BeforeEach + void setUp() throws IOException { + resultDir = rootDir.resolve("res"); + tempDir = rootDir.resolve("temp"); + Files.createDirectory(tempDir); + Files.createDirectory(resultDir); + } - @Test - void testUntarInvalid() { - assertThatThrownBy(() -> TarHelper.untar(Path.of(""), resultDir)); - } + @Test + void testUntarInvalid() { + assertThatThrownBy(() -> TarHelper.untar(Path.of(""), resultDir)); + } - @Test - void testUntar() throws IOException { - Path test = tempDir.resolve("Test"); - Files.createFile(test); - Files.setPosixFilePermissions( - test, - Set.of( - PosixFilePermission.OWNER_EXECUTE, - PosixFilePermission.GROUP_WRITE, - PosixFilePermission.OTHERS_READ, - PosixFilePermission.OWNER_WRITE, - PosixFilePermission.OWNER_READ - ) - ); - Files.createDirectory(tempDir.resolve("test2")); - Files.createFile(tempDir.resolve("test2").resolve("nested")); - Files.createDirectory(tempDir.resolve("test3")); - Files.createFile(tempDir.resolve("test3").resolve("nested3")); + @Test + void testUntar() throws IOException { + Path test = tempDir.resolve("Test"); + Files.createFile(test); + Files.setPosixFilePermissions( + test, + Set.of( + PosixFilePermission.OWNER_EXECUTE, + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.OWNER_READ)); + Files.createDirectory(tempDir.resolve("test2")); + Files.createFile(tempDir.resolve("test2").resolve("nested")); + Files.createDirectory(tempDir.resolve("test3")); + Files.createFile(tempDir.resolve("test3").resolve("nested3")); - tarTemp(); + tarTemp(); - TarHelper.untar(tempDir.resolve("output"), resultDir); - Files.deleteIfExists(tempDir.resolve("output")); + TarHelper.untar(tempDir.resolve("output"), resultDir); + Files.deleteIfExists(tempDir.resolve("output")); - List paths = Files.walk(tempDir).collect(Collectors.toList()); - for (Path path : paths) { - Path other = resultDir.resolve(tempDir.relativize(path)); - assertThat(Files.exists(other)).isTrue(); - assertThat(Files.size(other)).isEqualTo(Files.size(path)); - assertThat(Files.getPosixFilePermissions(other)) - .containsExactlyElementsOf( - Files.getPosixFilePermissions(path) - ); - } - } + List paths = Files.walk(tempDir).collect(Collectors.toList()); + for (Path path : paths) { + Path other = resultDir.resolve(tempDir.relativize(path)); + assertThat(Files.exists(other)).isTrue(); + assertThat(Files.size(other)).isEqualTo(Files.size(path)); + assertThat(Files.getPosixFilePermissions(other)) + .containsExactlyElementsOf(Files.getPosixFilePermissions(path)); + } + } - private void tarTemp() throws IOException { - TarArchiveOutputStream output = new TarArchiveOutputStream( - Files.newOutputStream(tempDir.resolve("output")) - ); - int dirCounter = 0; - try (output) { - List files = Files.walk(tempDir) - .filter(Predicate.not(tempDir.resolve("output")::equals)) - .collect(Collectors.toList()); - for (Path it : files) { - String fileName = tempDir.relativize(it).toString(); - TarArchiveEntry entry = new TarArchiveEntry( - it.toFile(), - fileName - ); + private void tarTemp() throws IOException { + TarArchiveOutputStream output = + new TarArchiveOutputStream(Files.newOutputStream(tempDir.resolve("output"))); + int dirCounter = 0; + try (output) { + List files = + Files.walk(tempDir) + .filter(Predicate.not(tempDir.resolve("output")::equals)) + .collect(Collectors.toList()); + for (Path it : files) { + String fileName = tempDir.relativize(it).toString(); + TarArchiveEntry entry = new TarArchiveEntry(it.toFile(), fileName); - if (Files.isDirectory(it)) { - // Also contain files/directories with no parent - if (dirCounter++ % 2 == 0) { - output.putArchiveEntry(entry); - output.closeArchiveEntry(); - } - continue; - } + if (Files.isDirectory(it)) { + // Also contain files/directories with no parent + if (dirCounter++ % 2 == 0) { + output.putArchiveEntry(entry); + output.closeArchiveEntry(); + } + continue; + } - entry.setMode(PermissionsHelper.toOctal(Files.getPosixFilePermissions(it))); + entry.setMode(PermissionsHelper.toOctal(Files.getPosixFilePermissions(it))); - try (InputStream inputStream = Files.newInputStream(it)) { - output.putArchiveEntry(entry); - IOUtils.copy(inputStream, output); - output.closeArchiveEntry(); - } - } - } - } -} \ No newline at end of file + try (InputStream inputStream = Files.newInputStream(it)) { + output.putArchiveEntry(entry); + IOUtils.copy(inputStream, output); + output.closeArchiveEntry(); + } + } + } + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutorTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutorTest.java index 3f2b6fc41..f39444b1c 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutorTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/execution/ProgramExecutorTest.java @@ -15,186 +15,189 @@ class ProgramExecutorTest { - private ProgramExecutor programExecutor; - private final int timeToForceKillMillis = 2000; - - @BeforeEach - void setUp() { - programExecutor = new ProgramExecutor(timeToForceKillMillis); - } - - @Test - void capturesExitCode() throws ExecutionException, InterruptedException { - ProgramResult result = programExecutor.execute("/usr/bin/env", "bash", "-c", "exit 120") - .get(); - assertThat(result.getExitCode()).isEqualTo(120); - } - - @Test - void capturesStandardOutput() throws ExecutionException, InterruptedException { - String output = "HEY you there\nMy äö߇friend"; - ProgramResult result = programExecutor.execute( - "/usr/bin/env", "bash", "-c", "echo -ne '" + output + "'" - ) - .get(); - assertThat(result.getStdOut()).isEqualTo(output); - assertThat(result.getStdErr()).isEmpty(); - } - - @Test - void capturesStandardError() throws ExecutionException, InterruptedException { - String output = "HEY you there\nMy äö߇friend"; - ProgramResult result = programExecutor.execute( - "/usr/bin/env", "bash", "-c", "echo -ne '" + output + "' 1>&2" - ) - .get(); - assertThat(result.getStdErr()).isEqualTo(output); - assertThat(result.getStdOut()).isEmpty(); - } - - @Test - void drainsStandardOut() throws ExecutionException, InterruptedException { - String output = "Hello".repeat(10_000); - ProgramResult result = programExecutor.execute( - "/usr/bin/env", "bash", "-c", "echo -ne '" + output + "'" - ) - .get(); - assertThat(result.getStdOut()).isEqualTo(output); - assertThat(result.getStdErr()).isEmpty(); - } - - @Test - void drainsStandardErr() throws ExecutionException, InterruptedException { - String output = "Hello".repeat(200_000); - Future future = programExecutor.execute( - "/usr/bin/env", "bash", "-c", - "for i in {1..200000}; do echo -ne 'Hello' ; done | tee /dev/stderr" - ); - - Thread thread = new Thread(() -> { - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(20)); - future.cancel(true); - } catch (InterruptedException ignored) { - // Reached when everything works - } - }); - thread.start(); - - ProgramResult result; - try { - result = future.get(); - } catch (CancellationException e) { - fail("Likely needed to be killed", e); - return; - } - - thread.interrupt(); - assertThat(result.getStdErr()).isEqualTo(output); - assertThat(result.getStdOut()).isEqualTo(output); - } - - @Test - void callInvalidProgram() { - assertThatThrownBy( - () -> programExecutor.execute("hello world how are you").get() - ); - } - - @Test - void gracefullyStopProgram() throws InterruptedException { - Future future = programExecutor.execute( - "/usr/bin/env", "bash", "-c", "stuff() {\n" - + " while true ; do\n" - + " /usr/bin/sleep 1\n" - + " done\n" - + "}\n" - + "\n" - + "stuff\n" - ); - Thread.sleep(100); - - future.cancel(true); - - Instant start = Instant.now(); - - assertThatThrownBy(future::get).isInstanceOf(CancellationException.class); - - assertThat(Duration.between(start, Instant.now()).toMillis()) - .isLessThan(timeToForceKillMillis); - } - - @Test - void forcefullyStopProgram() throws InterruptedException { - Future future = programExecutor.execute( - "/usr/bin/env", "bash", "-c", "stuff() {\n" - + " while true ; do\n" - + " /usr/bin/sleep 1\n" - + " done\n" - + "}\n" - + "\n" - + "trap \"stuff\" SIGTERM\n" - + "\n" - + "stuff\n" - ); - - Thread.sleep(100); - future.cancel(true); - - Instant start = Instant.now(); - - assertThatThrownBy(future::get).isInstanceOf(CancellationException.class); - - assertThat(Duration.between(start, Instant.now()).toMillis()) - .isGreaterThanOrEqualTo(timeToForceKillMillis); - } - - @Test - void hasSomewhatCorrectDuration() throws ExecutionException, InterruptedException { - ProgramExecutor executor = new ProgramExecutor(); - ProgramResult result = executor.execute("/usr/bin/env", "bash", "-c", "sleep 3") - .get(); - - assertThat(result.getRuntime().toSeconds()).isBetween(3L, 6L); - } - - @Test - void streamsStandardOutAndError() throws InterruptedException, ExecutionException { - ProgramExecutor executor = new ProgramExecutor(); - StreamsProcessOutput processOutput = executor.execute("/usr/bin/env", "bash", - "-c", - "echo 'Hello world'\n" - + "echo 'Hello world error' >&2\n" - + "\n" - + "sleep 2\n" - + "\n" - + "echo -n ' after!'\n" - + "echo -n ' after error!' >&2\n" - ); - - Thread.sleep(1000); - - assertThat(processOutput.getCurrentStdOut()).isEqualTo("Hello world\n"); - assertThat(processOutput.getCurrentStdErr()).isEqualTo("Hello world error\n"); - - // If the build server is slow, this can take a few seconds. Give it 6 and bail out early if - // it is faster - for (int i = 0; i < 12; i++) { - Thread.sleep(500); - - if (!processOutput.getCurrentStdOut().contains("after")) { - continue; - } - if (!processOutput.getCurrentStdErr().contains("after")) { - continue; - } - break; - } - - assertThat(processOutput.getCurrentStdOut()).isEqualTo("Hello world\n after!"); - assertThat(processOutput.getCurrentStdErr()).isEqualTo("Hello world error\n after error!"); - - assertThat(processOutput.isDone()); - assertThat(processOutput.get().getExitCode()).isEqualTo(0); - } + private ProgramExecutor programExecutor; + private final int timeToForceKillMillis = 2000; + + @BeforeEach + void setUp() { + programExecutor = new ProgramExecutor(timeToForceKillMillis); + } + + @Test + void capturesExitCode() throws ExecutionException, InterruptedException { + ProgramResult result = programExecutor.execute("/usr/bin/env", "bash", "-c", "exit 120").get(); + assertThat(result.getExitCode()).isEqualTo(120); + } + + @Test + void capturesStandardOutput() throws ExecutionException, InterruptedException { + String output = "HEY you there\nMy äö߇friend"; + ProgramResult result = + programExecutor.execute("/usr/bin/env", "bash", "-c", "echo -ne '" + output + "'").get(); + assertThat(result.getStdOut()).isEqualTo(output); + assertThat(result.getStdErr()).isEmpty(); + } + + @Test + void capturesStandardError() throws ExecutionException, InterruptedException { + String output = "HEY you there\nMy äö߇friend"; + ProgramResult result = + programExecutor + .execute("/usr/bin/env", "bash", "-c", "echo -ne '" + output + "' 1>&2") + .get(); + assertThat(result.getStdErr()).isEqualTo(output); + assertThat(result.getStdOut()).isEmpty(); + } + + @Test + void drainsStandardOut() throws ExecutionException, InterruptedException { + String output = "Hello".repeat(10_000); + ProgramResult result = + programExecutor.execute("/usr/bin/env", "bash", "-c", "echo -ne '" + output + "'").get(); + assertThat(result.getStdOut()).isEqualTo(output); + assertThat(result.getStdErr()).isEmpty(); + } + + @Test + void drainsStandardErr() throws ExecutionException, InterruptedException { + String output = "Hello".repeat(200_000); + Future future = + programExecutor.execute( + "/usr/bin/env", + "bash", + "-c", + "for i in {1..200000}; do echo -ne 'Hello' ; done | tee /dev/stderr"); + + Thread thread = + new Thread( + () -> { + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(20)); + future.cancel(true); + } catch (InterruptedException ignored) { + // Reached when everything works + } + }); + thread.start(); + + ProgramResult result; + try { + result = future.get(); + } catch (CancellationException e) { + fail("Likely needed to be killed", e); + return; + } + + thread.interrupt(); + assertThat(result.getStdErr()).isEqualTo(output); + assertThat(result.getStdOut()).isEqualTo(output); + } + + @Test + void callInvalidProgram() { + assertThatThrownBy(() -> programExecutor.execute("hello world how are you").get()); + } + + @Test + void gracefullyStopProgram() throws InterruptedException { + Future future = + programExecutor.execute( + "/usr/bin/env", + "bash", + "-c", + "stuff() {\n" + + " while true ; do\n" + + " /usr/bin/sleep 1\n" + + " done\n" + + "}\n" + + "\n" + + "stuff\n"); + Thread.sleep(100); + + future.cancel(true); + + Instant start = Instant.now(); + + assertThatThrownBy(future::get).isInstanceOf(CancellationException.class); + + assertThat(Duration.between(start, Instant.now()).toMillis()).isLessThan(timeToForceKillMillis); + } + + @Test + void forcefullyStopProgram() throws InterruptedException { + Future future = + programExecutor.execute( + "/usr/bin/env", + "bash", + "-c", + "stuff() {\n" + + " while true ; do\n" + + " /usr/bin/sleep 1\n" + + " done\n" + + "}\n" + + "\n" + + "trap \"stuff\" SIGTERM\n" + + "\n" + + "stuff\n"); + + Thread.sleep(100); + future.cancel(true); + + Instant start = Instant.now(); + + assertThatThrownBy(future::get).isInstanceOf(CancellationException.class); + + assertThat(Duration.between(start, Instant.now()).toMillis()) + .isGreaterThanOrEqualTo(timeToForceKillMillis); + } + + @Test + void hasSomewhatCorrectDuration() throws ExecutionException, InterruptedException { + ProgramExecutor executor = new ProgramExecutor(); + ProgramResult result = executor.execute("/usr/bin/env", "bash", "-c", "sleep 3").get(); + + assertThat(result.getRuntime().toSeconds()).isBetween(3L, 6L); + } + + @Test + void streamsStandardOutAndError() throws InterruptedException, ExecutionException { + ProgramExecutor executor = new ProgramExecutor(); + StreamsProcessOutput processOutput = + executor.execute( + "/usr/bin/env", + "bash", + "-c", + "echo 'Hello world'\n" + + "echo 'Hello world error' >&2\n" + + "\n" + + "sleep 2\n" + + "\n" + + "echo -n ' after!'\n" + + "echo -n ' after error!' >&2\n"); + + Thread.sleep(1000); + + assertThat(processOutput.getCurrentStdOut()).isEqualTo("Hello world\n"); + assertThat(processOutput.getCurrentStdErr()).isEqualTo("Hello world error\n"); + + // If the build server is slow, this can take a few seconds. Give it 6 and bail out early if + // it is faster + for (int i = 0; i < 12; i++) { + Thread.sleep(500); + + if (!processOutput.getCurrentStdOut().contains("after")) { + continue; + } + if (!processOutput.getCurrentStdErr().contains("after")) { + continue; + } + break; + } + + assertThat(processOutput.getCurrentStdOut()).isEqualTo("Hello world\n after!"); + assertThat(processOutput.getCurrentStdErr()).isEqualTo("Hello world error\n after error!"); + + assertThat(processOutput.isDone()); + assertThat(processOutput.get().getExitCode()).isEqualTo(0); + } } diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParserTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParserTest.java index 5664a6f1f..980b882ea 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParserTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CoreInfoParserTest.java @@ -8,89 +8,80 @@ class CoreInfoParserTest { - private static final List NORMAL_TEST_DATA = List.of( - "processor : 0", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "core id : 0", - "physical id : 0", - "cpu cores : 4", - "", + private static final List NORMAL_TEST_DATA = + List.of( + "processor : 0", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "core id : 0", + "physical id : 0", + "cpu cores : 4", + "", + "processor : 1", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 4", + "", + "processor : 2", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 4", + "", + "processor : 3", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 4", + "", + "processor : 4", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4", + "", + "processor : 5", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4", + "", + "processor : 6", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4", + "", + "processor : 7", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4"); - "processor : 1", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 4", - "", + @Test + void returnsEmptyListForNoInput() { + List infos = new CoreInfoParser().coreInfos(Collections.emptyList()); + assertThat(infos).isEmpty(); + } - "processor : 2", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 4", - "", + @Test + void usesDefaults() { + List infos = new CoreInfoParser().coreInfos(List.of("hey")); + assertThat(infos).containsExactly(new CoreInfo(-1, -1, -1, -1, "N/A")); + } - "processor : 3", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 4", - "", - - "processor : 4", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4", - "", - - "processor : 5", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4", - "", - - "processor : 6", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4", - "", - - "processor : 7", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4" - ); - - @Test - void returnsEmptyListForNoInput() { - List infos = new CoreInfoParser().coreInfos(Collections.emptyList()); - assertThat(infos).isEmpty(); - } - - @Test - void usesDefaults() { - List infos = new CoreInfoParser().coreInfos(List.of("hey")); - assertThat(infos).containsExactly( - new CoreInfo(-1, -1, -1, -1, "N/A") - ); - } - - @Test - void parsesTestCores() { - List infos = new CoreInfoParser().coreInfos(NORMAL_TEST_DATA); - assertThat(infos).containsExactlyInAnyOrder( - new CoreInfo(0, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(1, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(2, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(3, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(4, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(5, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(6, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(7, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz") - ); - } -} \ No newline at end of file + @Test + void parsesTestCores() { + List infos = new CoreInfoParser().coreInfos(NORMAL_TEST_DATA); + assertThat(infos) + .containsExactlyInAnyOrder( + new CoreInfo(0, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(1, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(2, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(3, 0, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(4, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(5, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(6, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(7, 1, 0, 4, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz")); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfoTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfoTest.java index c082cc828..b5657e292 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfoTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/CpuInfoTest.java @@ -9,154 +9,140 @@ class CpuInfoTest { - private static final List NORMAL_TEST_DATA = List.of( - "processor : 0", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "core id : 0", - "physical id : 0", - "cpu cores : 4", - "", - - "processor : 1", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 4", - "", - - "processor : 2", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 4", - "", - - "processor : 3", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 4", - "", - - "processor : 4", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4", - "", - - "processor : 5", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4", - "", - - "processor : 6", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4", - "", - - "processor : 7", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 1", - "cpu cores : 4" - ); - private CpuInfo cpuInfo; - - @BeforeEach - void setUp() { - cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(NORMAL_TEST_DATA)); - } - - @Test - void testCounts() { - assertThat(cpuInfo.virtualCoreCount()).isEqualTo(8); - assertThat(cpuInfo.physicalCoreCount()).isEqualTo(4); - assertThat(cpuInfo.cpuCount()).isEqualTo(1); - assertThat(cpuInfo.coreModels()) - .containsExactly("Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"); - } - - @Test - void testModelReturnsSet() { - List data = new ArrayList<>(NORMAL_TEST_DATA); - data.set(1, "model name: My cool model"); - cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(data)); - - assertThat(cpuInfo.coreModels()) - .containsExactly( - "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "My cool model"); - } - - @Test - void testMultipleCpusCountedCorrectly() { - List data = List.of( - "processor : 0", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 1", - "", - - "processor : 1", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 1", - "core id : 0", - "cpu cores : 1", - "", - - "processor : 2", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 1", - "core id : 1", - "cpu cores : 1", - "" - ); - cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(data)); - - assertThat(cpuInfo.coreModels()) - .containsExactly("Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"); - assertThat(cpuInfo.cpuCount()).isEqualTo(2); - assertThat(cpuInfo.physicalCoreCount()).isEqualTo(2); - assertThat(cpuInfo.virtualCoreCount()).isEqualTo(3); - } - - @Test - void testEqualsCoreInfos() { - List data = List.of( - "processor : 0", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 0", - "core id : 0", - "cpu cores : 1", - "", - - "processor : 1", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 1", - "core id : 0", - "cpu cores : 1", - "", - - "processor : 2", - "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", - "physical id : 1", - "core id : 1", - "cpu cores : 1", - "" - ); - cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(data)); - - assertThat(cpuInfo).isEqualTo(new CpuInfo(List.of( - new CoreInfo(0, 0, 0, 1, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(1, 0, 1, 1, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), - new CoreInfo(2, 1, 1, 1, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz") - )) - ); - } -} \ No newline at end of file + private static final List NORMAL_TEST_DATA = + List.of( + "processor : 0", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "core id : 0", + "physical id : 0", + "cpu cores : 4", + "", + "processor : 1", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 4", + "", + "processor : 2", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 4", + "", + "processor : 3", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 4", + "", + "processor : 4", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4", + "", + "processor : 5", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4", + "", + "processor : 6", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4", + "", + "processor : 7", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 1", + "cpu cores : 4"); + private CpuInfo cpuInfo; + + @BeforeEach + void setUp() { + cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(NORMAL_TEST_DATA)); + } + + @Test + void testCounts() { + assertThat(cpuInfo.virtualCoreCount()).isEqualTo(8); + assertThat(cpuInfo.physicalCoreCount()).isEqualTo(4); + assertThat(cpuInfo.cpuCount()).isEqualTo(1); + assertThat(cpuInfo.coreModels()).containsExactly("Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"); + } + + @Test + void testModelReturnsSet() { + List data = new ArrayList<>(NORMAL_TEST_DATA); + data.set(1, "model name: My cool model"); + cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(data)); + + assertThat(cpuInfo.coreModels()) + .containsExactly("Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", "My cool model"); + } + + @Test + void testMultipleCpusCountedCorrectly() { + List data = + List.of( + "processor : 0", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 1", + "", + "processor : 1", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 1", + "core id : 0", + "cpu cores : 1", + "", + "processor : 2", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 1", + "core id : 1", + "cpu cores : 1", + ""); + cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(data)); + + assertThat(cpuInfo.coreModels()).containsExactly("Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"); + assertThat(cpuInfo.cpuCount()).isEqualTo(2); + assertThat(cpuInfo.physicalCoreCount()).isEqualTo(2); + assertThat(cpuInfo.virtualCoreCount()).isEqualTo(3); + } + + @Test + void testEqualsCoreInfos() { + List data = + List.of( + "processor : 0", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 0", + "core id : 0", + "cpu cores : 1", + "", + "processor : 1", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 1", + "core id : 0", + "cpu cores : 1", + "", + "processor : 2", + "model name : Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz", + "physical id : 1", + "core id : 1", + "cpu cores : 1", + ""); + cpuInfo = new CpuInfo(new CoreInfoParser().coreInfos(data)); + + assertThat(cpuInfo) + .isEqualTo( + new CpuInfo( + List.of( + new CoreInfo(0, 0, 0, 1, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(1, 0, 1, 1, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz"), + new CoreInfo(2, 1, 1, 1, "Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz")))); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfoTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfoTest.java index 64580e752..afeb76a44 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfoTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/LinuxSystemInfoTest.java @@ -9,32 +9,31 @@ class LinuxSystemInfoTest { - private LinuxSystemInfo systemInfo; - - @BeforeEach - void setUp() { - systemInfo = LinuxSystemInfo.getCurrent(); - } - - @Test - void hasPlausibleCpuInfo() { - assumeFalse(OSCheck.isStupidWindows()); - - assertThat(systemInfo.getCpuInfo().cpuCount()).isGreaterThan(0); - assertThat(systemInfo.getCpuInfo().physicalCoreCount()).isGreaterThan(0); - assertThat(systemInfo.getCpuInfo().coreModels()).hasSizeGreaterThanOrEqualTo(1); - - assertThat(systemInfo.getCpuInfo().virtualCoreCount()) - .isGreaterThanOrEqualTo(Runtime.getRuntime().availableProcessors()); - - } - - @Test - void hasPlausibleMemoryInfo() { - assumeFalse(OSCheck.isStupidWindows()); - - assertThat(systemInfo.getMemoryInfo().freeMemoryKiB()).isGreaterThanOrEqualTo(0); - assertThat(systemInfo.getMemoryInfo().totalMemoryKib()) - .isGreaterThanOrEqualTo(Runtime.getRuntime().maxMemory() / 1024); - } -} \ No newline at end of file + private LinuxSystemInfo systemInfo; + + @BeforeEach + void setUp() { + systemInfo = LinuxSystemInfo.getCurrent(); + } + + @Test + void hasPlausibleCpuInfo() { + assumeFalse(OSCheck.isStupidWindows()); + + assertThat(systemInfo.getCpuInfo().cpuCount()).isGreaterThan(0); + assertThat(systemInfo.getCpuInfo().physicalCoreCount()).isGreaterThan(0); + assertThat(systemInfo.getCpuInfo().coreModels()).hasSizeGreaterThanOrEqualTo(1); + + assertThat(systemInfo.getCpuInfo().virtualCoreCount()) + .isGreaterThanOrEqualTo(Runtime.getRuntime().availableProcessors()); + } + + @Test + void hasPlausibleMemoryInfo() { + assumeFalse(OSCheck.isStupidWindows()); + + assertThat(systemInfo.getMemoryInfo().freeMemoryKiB()).isGreaterThanOrEqualTo(0); + assertThat(systemInfo.getMemoryInfo().totalMemoryKib()) + .isGreaterThanOrEqualTo(Runtime.getRuntime().maxMemory() / 1024); + } +} diff --git a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfoTest.java b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfoTest.java index 0152e3b75..e1eba515b 100644 --- a/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfoTest.java +++ b/backend/shared/src/test/java/de/aaaaaaah/velcom/shared/util/systeminfo/MemoryInfoTest.java @@ -8,40 +8,41 @@ class MemoryInfoTest { - @Test - void usesMinusOneForEmptyList() { - MemoryInfo meminfo = MemoryInfo.fromMeminfo(Collections.emptyList()); - assertThat(meminfo.freeMemoryKiB()).isEqualTo(-1); - assertThat(meminfo.totalMemoryKib()).isEqualTo(-1); - } + @Test + void usesMinusOneForEmptyList() { + MemoryInfo meminfo = MemoryInfo.fromMeminfo(Collections.emptyList()); + assertThat(meminfo.freeMemoryKiB()).isEqualTo(-1); + assertThat(meminfo.totalMemoryKib()).isEqualTo(-1); + } - @Test - void usesMinusOneForInvalidList() { - MemoryInfo meminfo = MemoryInfo.fromMeminfo(List.of("hello", "there")); - assertThat(meminfo.freeMemoryKiB()).isEqualTo(-1); - assertThat(meminfo.totalMemoryKib()).isEqualTo(-1); - } + @Test + void usesMinusOneForInvalidList() { + MemoryInfo meminfo = MemoryInfo.fromMeminfo(List.of("hello", "there")); + assertThat(meminfo.freeMemoryKiB()).isEqualTo(-1); + assertThat(meminfo.totalMemoryKib()).isEqualTo(-1); + } - @Test - void usesMinusOneForListWithInvalidNumber() { - MemoryInfo meminfo = MemoryInfo.fromMeminfo(List.of("MemTotal: 2000 kB", "MemFree: shdsj")); - assertThat(meminfo.freeMemoryKiB()).isEqualTo(-1); - assertThat(meminfo.totalMemoryKib()).isEqualTo(2000); - } + @Test + void usesMinusOneForListWithInvalidNumber() { + MemoryInfo meminfo = MemoryInfo.fromMeminfo(List.of("MemTotal: 2000 kB", "MemFree: shdsj")); + assertThat(meminfo.freeMemoryKiB()).isEqualTo(-1); + assertThat(meminfo.totalMemoryKib()).isEqualTo(2000); + } - @Test - void parseValid() { - MemoryInfo meminfo = MemoryInfo.fromMeminfo(List.of( - "Hugepagesize: 2048 kB", - "Hugetlb: 0 kB", - "MemTotal: 16247728 kB", - "MemFree: 6526572 kB", - "MemAvailable: 10049620 kB", - "DirectMap4k: 260468 kB", - "DirectMap2M: 13219840 kB", - "DirectMap1G: 3145728 kB" - )); - assertThat(meminfo.freeMemoryKiB()).isEqualTo(6526572); - assertThat(meminfo.totalMemoryKib()).isEqualTo(16247728); - } -} \ No newline at end of file + @Test + void parseValid() { + MemoryInfo meminfo = + MemoryInfo.fromMeminfo( + List.of( + "Hugepagesize: 2048 kB", + "Hugetlb: 0 kB", + "MemTotal: 16247728 kB", + "MemFree: 6526572 kB", + "MemAvailable: 10049620 kB", + "DirectMap4k: 260468 kB", + "DirectMap2M: 13219840 kB", + "DirectMap1G: 3145728 kB")); + assertThat(meminfo.freeMemoryKiB()).isEqualTo(6526572); + assertThat(meminfo.totalMemoryKib()).isEqualTo(16247728); + } +} diff --git a/frontend/README.md b/frontend/README.md index b9c525588..9b07b06eb 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,24 +1,29 @@ # frontend ## Project setup + ``` yarn install ``` ### Compiles and hot-reloads for development + ``` yarn serve ``` ### Compiles and minifies for production + ``` yarn build ``` ### Lints and fixes files + ``` yarn lint ``` ### Customize configuration + See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/frontend/babel.config.js b/frontend/babel.config.js index 716b0237c..162a3ea97 100644 --- a/frontend/babel.config.js +++ b/frontend/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: ['@vue/cli-plugin-babel/preset'] -} + presets: ["@vue/cli-plugin-babel/preset"], +}; diff --git a/frontend/public/fonts/fontface-declaration.css b/frontend/public/fonts/fontface-declaration.css index 7183e6c83..2091b740d 100644 --- a/frontend/public/fonts/fontface-declaration.css +++ b/frontend/public/fonts/fontface-declaration.css @@ -1,238 +1,215 @@ /* roboto-100 - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 100; - src: url('../fonts/roboto-v20-latin_latin-ext-100.eot'); /* IE9 Compat Modes */ - src: local('Roboto Thin'), local('Roboto-Thin'), - url('../fonts/roboto-v20-latin_latin-ext-100.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-100.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-100.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-100.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-100.svg#Roboto') format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-100.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Thin"), + local("Roboto-Thin"), + url("../fonts/roboto-v20-latin_latin-ext-100.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-100.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-100.woff") format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-100.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-100.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-100italic - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: italic; font-weight: 100; - src: url('../fonts/roboto-v20-latin_latin-ext-100italic.eot'); /* IE9 Compat Modes */ - src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'), - url('../fonts/roboto-v20-latin_latin-ext-100italic.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-100italic.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-100italic.woff') format('woff'), - /* Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-100italic.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-100italic.svg#Roboto') - format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-100italic.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Thin Italic"), + local("Roboto-ThinItalic"), + url("../fonts/roboto-v20-latin_latin-ext-100italic.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-100italic.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-100italic.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-100italic.ttf") + format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-100italic.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-300 - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 300; - src: url('../fonts/roboto-v20-latin_latin-ext-300.eot'); /* IE9 Compat Modes */ - src: local('Roboto Light'), local('Roboto-Light'), - url('../fonts/roboto-v20-latin_latin-ext-300.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-300.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-300.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-300.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-300.svg#Roboto') format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-300.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Light"), + local("Roboto-Light"), + url("../fonts/roboto-v20-latin_latin-ext-300.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-300.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-300.woff") format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-300.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-300.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-300italic - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: italic; font-weight: 300; - src: url('../fonts/roboto-v20-latin_latin-ext-300italic.eot'); /* IE9 Compat Modes */ - src: local('Roboto Light Italic'), local('Roboto-LightItalic'), - url('../fonts/roboto-v20-latin_latin-ext-300italic.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-300italic.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-300italic.woff') format('woff'), - /* Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-300italic.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-300italic.svg#Roboto') - format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-300italic.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Light Italic"), + local("Roboto-LightItalic"), + url("../fonts/roboto-v20-latin_latin-ext-300italic.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-300italic.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-300italic.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-300italic.ttf") + format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-300italic.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-regular - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 400; - src: url('../fonts/roboto-v20-latin_latin-ext-regular.eot'); /* IE9 Compat Modes */ - src: local('Roboto'), local('Roboto-Regular'), - url('../fonts/roboto-v20-latin_latin-ext-regular.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-regular.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-regular.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-regular.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-regular.svg#Roboto') - format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-regular.eot"); /* IE9 Compat Modes */ + src: + local("Roboto"), + local("Roboto-Regular"), + url("../fonts/roboto-v20-latin_latin-ext-regular.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-regular.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-regular.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-regular.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-regular.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-italic - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: italic; font-weight: 400; - src: url('../fonts/roboto-v20-latin_latin-ext-italic.eot'); /* IE9 Compat Modes */ - src: local('Roboto Italic'), local('Roboto-Italic'), - url('../fonts/roboto-v20-latin_latin-ext-italic.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-italic.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-italic.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-italic.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-italic.svg#Roboto') format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-italic.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Italic"), + local("Roboto-Italic"), + url("../fonts/roboto-v20-latin_latin-ext-italic.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-italic.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-italic.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-italic.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-italic.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-500 - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 500; - src: url('../fonts/roboto-v20-latin_latin-ext-500.eot'); /* IE9 Compat Modes */ - src: local('Roboto Medium'), local('Roboto-Medium'), - url('../fonts/roboto-v20-latin_latin-ext-500.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-500.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-500.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-500.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-500.svg#Roboto') format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-500.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Medium"), + local("Roboto-Medium"), + url("../fonts/roboto-v20-latin_latin-ext-500.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-500.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-500.woff") format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-500.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-500.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-500italic - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: italic; font-weight: 500; - src: url('../fonts/roboto-v20-latin_latin-ext-500italic.eot'); /* IE9 Compat Modes */ - src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), - url('../fonts/roboto-v20-latin_latin-ext-500italic.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-500italic.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-500italic.woff') format('woff'), - /* Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-500italic.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-500italic.svg#Roboto') - format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-500italic.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Medium Italic"), + local("Roboto-MediumItalic"), + url("../fonts/roboto-v20-latin_latin-ext-500italic.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-500italic.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-500italic.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-500italic.ttf") + format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-500italic.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-700 - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 700; - src: url('../fonts/roboto-v20-latin_latin-ext-700.eot'); /* IE9 Compat Modes */ - src: local('Roboto Bold'), local('Roboto-Bold'), - url('../fonts/roboto-v20-latin_latin-ext-700.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-700.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-700.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-700.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-700.svg#Roboto') format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-700.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Bold"), + local("Roboto-Bold"), + url("../fonts/roboto-v20-latin_latin-ext-700.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-700.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-700.woff") format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-700.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-700.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-700italic - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: italic; font-weight: 700; - src: url('../fonts/roboto-v20-latin_latin-ext-700italic.eot'); /* IE9 Compat Modes */ - src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), - url('../fonts/roboto-v20-latin_latin-ext-700italic.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-700italic.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-700italic.woff') format('woff'), - /* Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-700italic.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-700italic.svg#Roboto') - format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-700italic.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Bold Italic"), + local("Roboto-BoldItalic"), + url("../fonts/roboto-v20-latin_latin-ext-700italic.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-700italic.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-700italic.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-700italic.ttf") + format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-700italic.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-900 - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: normal; font-weight: 900; - src: url('../fonts/roboto-v20-latin_latin-ext-900.eot'); /* IE9 Compat Modes */ - src: local('Roboto Black'), local('Roboto-Black'), - url('../fonts/roboto-v20-latin_latin-ext-900.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-900.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-900.woff') format('woff'), - /* Modern Browsers */ url('../fonts/roboto-v20-latin_latin-ext-900.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-900.svg#Roboto') format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-900.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Black"), + local("Roboto-Black"), + url("../fonts/roboto-v20-latin_latin-ext-900.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-900.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-900.woff") format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-900.ttf") format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-900.svg#Roboto") + format("svg"); /* Legacy iOS */ } /* roboto-900italic - latin_latin-ext */ @font-face { - font-family: 'Roboto'; + font-family: "Roboto"; font-style: italic; font-weight: 900; - src: url('../fonts/roboto-v20-latin_latin-ext-900italic.eot'); /* IE9 Compat Modes */ - src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), - url('../fonts/roboto-v20-latin_latin-ext-900italic.eot?#iefix') - format('embedded-opentype'), - /* IE6-IE8 */ url('../fonts/roboto-v20-latin_latin-ext-900italic.woff2') - format('woff2'), - /* Super Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-900italic.woff') format('woff'), - /* Modern Browsers */ - url('../fonts/roboto-v20-latin_latin-ext-900italic.ttf') - format('truetype'), - /* Safari, Android, iOS */ - url('../fonts/roboto-v20-latin_latin-ext-900italic.svg#Roboto') - format('svg'); /* Legacy iOS */ + src: url("../fonts/roboto-v20-latin_latin-ext-900italic.eot"); /* IE9 Compat Modes */ + src: + local("Roboto Black Italic"), + local("Roboto-BlackItalic"), + url("../fonts/roboto-v20-latin_latin-ext-900italic.eot?#iefix") format("embedded-opentype"), + /* IE6-IE8 */ url("../fonts/roboto-v20-latin_latin-ext-900italic.woff2") format("woff2"), + /* Super Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-900italic.woff") + format("woff"), + /* Modern Browsers */ url("../fonts/roboto-v20-latin_latin-ext-900italic.ttf") + format("truetype"), + /* Safari, Android, iOS */ url("../fonts/roboto-v20-latin_latin-ext-900italic.svg#Roboto") + format("svg"); /* Legacy iOS */ } diff --git a/frontend/public/index.html b/frontend/public/index.html index 08b8833ae..a637c1d91 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,4 +1,4 @@ - + @@ -6,11 +6,7 @@ VelCom - +