001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.benchmarking.common.output; 017 018import lombok.Getter; 019 020import java.io.IOException; 021import java.io.UncheckedIOException; 022import java.nio.file.Files; 023import java.nio.file.Path; 024import java.util.Comparator; 025import java.util.Objects; 026 027/** 028 * Manages the output directory structure for benchmark results. 029 * Provides clear separation between deployable content (gh-pages-ready) 030 * and non-deployable raw data (history, prometheus, wrk). 031 * <p> 032 * Directory structure: 033 * <pre> 034 * benchmark-results/ 035 * ├── gh-pages-ready/ (deploymentDir, htmlDir) - All content deployable to GitHub Pages 036 * │ ├── data/ (dataDir) - JSON data files 037 * │ ├── badges/ (badgesDir) - Badge JSON files 038 * │ └── api/ (apiDir) - API endpoint JSON files 039 * ├── history/ (historyDir) - Historical archive data (NOT deployed) 040 * ├── prometheus/ (prometheusRawDir) - Raw Prometheus metrics (NOT deployed) 041 * └── wrk/ (wrkDir) - WRK raw results (NOT deployed) 042 * </pre> 043 */ 044public class OutputDirectoryStructure { 045 046 /** Root benchmark-results directory */ 047 @Getter 048 private final Path benchmarkResultsDir; 049 050 /** 051 * Deployment directory (gh-pages-ready) - all content in this directory 052 * is deployable to GitHub Pages 053 */ 054 @Getter 055 private final Path deploymentDir; 056 057 /** 058 * HTML directory (same as deployment directory) - HTML files should be 059 * written directly to this directory 060 */ 061 @Getter 062 private final Path htmlDir; 063 064 /** Data directory (gh-pages-ready/data) - JSON data files location */ 065 @Getter 066 private final Path dataDir; 067 068 /** Badges directory (gh-pages-ready/badges) - Badge JSON files location */ 069 @Getter 070 private final Path badgesDir; 071 072 /** API directory (gh-pages-ready/api) - API endpoint JSON files location */ 073 @Getter 074 private final Path apiDir; 075 076 // Non-deployed directories (in benchmark-results root) 077 private final Path historyDir; // benchmark-results/history 078 private final Path prometheusRawDir; // benchmark-results/prometheus 079 private final Path wrkDir; // benchmark-results/wrk (WRK module only) 080 081 /** 082 * Creates a new output directory structure. 083 * 084 * @param benchmarkResultsDir the root benchmark-results directory 085 * @throws NullPointerException if benchmarkResultsDir is null 086 */ 087 public OutputDirectoryStructure(Path benchmarkResultsDir) { 088 this.benchmarkResultsDir = Objects.requireNonNull(benchmarkResultsDir, 089 "benchmarkResultsDir must not be null"); 090 this.deploymentDir = benchmarkResultsDir.resolve("gh-pages-ready"); 091 this.htmlDir = deploymentDir; 092 this.dataDir = deploymentDir.resolve("data"); 093 this.badgesDir = deploymentDir.resolve("badges"); 094 this.apiDir = deploymentDir.resolve("api"); 095 096 // Non-deployed directories stay in root 097 this.historyDir = benchmarkResultsDir.resolve("history"); 098 this.prometheusRawDir = benchmarkResultsDir.resolve("prometheus"); 099 this.wrkDir = benchmarkResultsDir.resolve("wrk"); 100 } 101 102 /** 103 * Ensures deployment directories exist, creating them if necessary. 104 * Non-deployed directories (history, prometheus, wrk) are created only when accessed. 105 * 106 * @throws IOException if directory creation fails 107 */ 108 public void ensureDirectories() throws IOException { 109 // Create deployment directories (always needed) 110 Files.createDirectories(deploymentDir); 111 Files.createDirectories(dataDir); 112 Files.createDirectories(badgesDir); 113 Files.createDirectories(apiDir); 114 115 // Non-deployed directories are created on-demand by individual getters 116 // to avoid creating unused directories in modules that don't need them 117 } 118 119 /** 120 * Gets the history directory (benchmark-results/history). 121 * Historical archive data should be stored in this directory. 122 * This directory is NOT deployed to GitHub Pages. 123 * Creates the directory if it doesn't exist. 124 * 125 * @return the history directory path 126 * @throws IOException if directory creation fails 127 */ 128 public Path getHistoryDir() throws IOException { 129 Files.createDirectories(historyDir); 130 return historyDir; 131 } 132 133 /** 134 * Gets the raw Prometheus metrics directory (benchmark-results/prometheus). 135 * Raw Prometheus data should be stored in this directory. 136 * This directory is NOT deployed to GitHub Pages. 137 * Creates the directory if it doesn't exist. 138 * 139 * @return the raw Prometheus directory path 140 * @throws IOException if directory creation fails 141 */ 142 public Path getPrometheusRawDir() throws IOException { 143 Files.createDirectories(prometheusRawDir); 144 return prometheusRawDir; 145 } 146 147 /** 148 * Gets the WRK directory (benchmark-results/wrk). 149 * WRK raw results should be stored in this directory. 150 * This directory is NOT deployed to GitHub Pages. 151 * Creates the directory if it doesn't exist. 152 * 153 * @return the WRK directory path 154 * @throws IOException if directory creation fails 155 */ 156 public Path getWrkDir() throws IOException { 157 Files.createDirectories(wrkDir); 158 return wrkDir; 159 } 160 161 /** 162 * Checks if the deployment directory exists. 163 * 164 * @return true if the deployment directory exists, false otherwise 165 */ 166 public boolean isDeploymentDirectoryExists() { 167 return Files.exists(deploymentDir); 168 } 169 170 /** 171 * Cleans the deployment directory by deleting it and recreating it. 172 * This ensures a fresh start for report generation. 173 * 174 * @throws IOException if deletion or creation fails 175 */ 176 public void cleanDeploymentDirectory() throws IOException { 177 if (Files.exists(deploymentDir)) { 178 deleteDirectoryRecursively(deploymentDir); 179 } 180 Files.createDirectories(deploymentDir); 181 Files.createDirectories(dataDir); 182 Files.createDirectories(badgesDir); 183 Files.createDirectories(apiDir); 184 } 185 186 /** 187 * Recursively deletes a directory and all its contents. 188 * 189 * @param path the directory to delete 190 * @throws IOException if deletion fails 191 */ 192 private void deleteDirectoryRecursively(Path path) throws IOException { 193 if (Files.exists(path)) { 194 try (var stream = Files.walk(path)) { 195 stream.sorted(Comparator.reverseOrder()) 196 .forEach(p -> { 197 try { 198 Files.delete(p); 199 } catch (IOException e) { 200 throw new UncheckedIOException("Failed to delete: " + p, e); 201 } 202 }); 203 } 204 } 205 } 206 207 @Override 208 public String toString() { 209 return "OutputDirectoryStructure{" + 210 "benchmarkResultsDir=" + benchmarkResultsDir + 211 ", deploymentDir=" + deploymentDir + 212 ", dataDir=" + dataDir + 213 ", badgesDir=" + badgesDir + 214 ", apiDir=" + apiDir + 215 ", historyDir=" + historyDir + 216 ", prometheusRawDir=" + prometheusRawDir + 217 ", wrkDir=" + wrkDir + 218 '}'; 219 } 220}