diff --git a/README.md b/README.md index 0060f5b..0107f74 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,36 @@ Agent started on port 7070 ### 3. Run the Master (Terminal 2) +#### Option A (recommended): run from config file + +Create a config file (JSON/YAML). Example `config.json`: + +```json +{ + "url": "https://httpbin.org/get", + "rps": 5, + "duration": 10 +} +``` + +Or `config.yaml`: + +```yaml +url: "https://httpbin.org/get" +rps: 5 +duration: 10 +``` + +Run: + +```bash +java -jar volta-master/target/volta-master-1.0-SNAPSHOT.jar \ + --config=./config.json \ + --agent=localhost:7070 +``` + +#### Option B (legacy, still supported): pass parameters via CLI flags + ```bash java -jar volta-master/target/volta-master-1.0-SNAPSHOT.jar \ --url=https://httpbin.org/get \ diff --git a/volta-master/pom.xml b/volta-master/pom.xml index 96e8b82..8ac96ec 100644 --- a/volta-master/pom.xml +++ b/volta-master/pom.xml @@ -68,6 +68,10 @@ mockito-junit-jupiter test + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + diff --git a/volta-master/src/main/java/com/volta/master/cli/ArgsParser.java b/volta-master/src/main/java/com/volta/master/cli/ArgsParser.java index dffb53d..06cfe3b 100644 --- a/volta-master/src/main/java/com/volta/master/cli/ArgsParser.java +++ b/volta-master/src/main/java/com/volta/master/cli/ArgsParser.java @@ -1,28 +1,77 @@ package com.volta.master.cli; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.volta.model.TestConfig; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.springframework.boot.ApplicationArguments; public class ArgsParser { private static final String USAGE = """ - Usage: java -jar volta-master.jar --url= --rps= --duration= --agent= - Example: java -jar volta-master.jar --url=https://httpbin.org/get --rps=10 --duration=30 --agent=localhost:7070 - """; + Usage: java -jar volta-master.jar --url= --rps= --duration= --agent= + java -jar volta-master.jar --config=./ --agent= + + Example: java -jar volta-master.jar --url=https://httpbin.org/get --rps=10 --duration=30 --agent=localhost:7070 + java -jar volta-master.jar --config=./config.json --agent=localhost:7070 + """; public static MasterArgs parse(ApplicationArguments args) { - String url = requireString(args, "url"); - int rps = requirePositiveInt(args, "rps"); - int duration = requirePositiveInt(args, "duration"); - String agent = requireString(args, "agent"); - validateUrl(url); + TestConfig testConfig; + if (args.containsOption("config")) { + String configPath = requireString(args, "config"); + + Path path = Path.of(configPath); + if (!Files.exists(path)) { + throw new ArgsException("Config file not found: " + configPath + "\n" + USAGE); + } + if (!Files.isRegularFile(path)) { + throw new ArgsException("Config path is not a regular file: " + configPath + "\n" + USAGE); + } + + ObjectMapper mapper; + String name = path.getFileName().toString().toLowerCase(); + if (name.endsWith(".json")) { + mapper = new ObjectMapper(); + } else if (name.endsWith(".yaml") || name.endsWith(".yml")) { + mapper = new ObjectMapper(new YAMLFactory()); + } else { + throw new ArgsException("Unsupported file format: " + configPath + "\n" + USAGE); + } + + try { + testConfig = mapper.readValue(path.toFile(), TestConfig.class); + } catch (IOException e) { + throw new ArgsException( + "Failed to read/parse config: " + configPath + "\n" + e.getMessage() + "\n" + USAGE); + } + + if (testConfig.rps() <= 0) { + throw new ArgsException("Argument rps in config must be positive\n" + USAGE); + } + if (testConfig.duration() <= 0) { + throw new ArgsException("Argument duration in config must be positive\n" + USAGE); + } + validateUrl(testConfig.url()); + + } else { + String url = requireString(args, "url"); + int rps = requirePositiveInt(args, "rps"); + int duration = requirePositiveInt(args, "duration"); + + validateUrl(url); + testConfig = new TestConfig(url, rps, duration); + } + String agent = requireString(args, "agent"); validateAgent(agent); agent = "http://" + agent; - return new MasterArgs(new TestConfig(url, rps, duration), agent); + return new MasterArgs(testConfig, agent); } private static String requireString(ApplicationArguments args, String name) { diff --git a/volta-master/src/test/java/com/volta/master/cli/ArgsParserTest.java b/volta-master/src/test/java/com/volta/master/cli/ArgsParserTest.java index dbb9572..47d646a 100644 --- a/volta-master/src/test/java/com/volta/master/cli/ArgsParserTest.java +++ b/volta-master/src/test/java/com/volta/master/cli/ArgsParserTest.java @@ -3,15 +3,25 @@ import static org.junit.jupiter.api.Assertions.*; import com.volta.model.TestConfig; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.DefaultApplicationArguments; -class CliArgsParserTest { +class ArgsParserTest { private static DefaultApplicationArguments args(String... rawArgs) { return new DefaultApplicationArguments(rawArgs); } + private static Path writeTempFile(Path dir, String fileName, String content) throws IOException { + Path path = dir.resolve(fileName); + Files.writeString(path, content); + return path; + } + @Test void validArgsProduceCorrectMasterArgs() { MasterArgs result = @@ -29,6 +39,159 @@ void validArgsProduceCorrectMasterArgs() { assertEquals("http://localhost:7070", result.agentUrl()); } + @Test + void configJsonProducesCorrectMasterArgs(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.json", + """ + { + "url": "https://httpbin.org/get", + "rps": 10, + "duration": 30 + } + """); + + MasterArgs result = + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070")); + + TestConfig config = result.testConfig(); + assertEquals("https://httpbin.org/get", config.url()); + assertEquals(10, config.rps()); + assertEquals(30, config.duration()); + assertEquals("http://localhost:7070", result.agentUrl()); + } + + @Test + void missingConfigFileThrows() { + assertThrows( + ArgsException.class, + () -> + ArgsParser.parse( + args("--config=./definitely-not-exists.json", "--agent=localhost:7070"))); + } + + @Test + void invalidJsonInConfigThrows(@TempDir Path tempDir) throws Exception { + Path configPath = writeTempFile(tempDir, "config.json", "{ this is not json }"); + + assertThrows( + ArgsException.class, + () -> + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070"))); + } + + @Test + void zeroRpsInConfigThrows(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.json", + """ + { "url": "https://example.com", "rps": 0, "duration": 10 } + """); + + assertThrows( + ArgsException.class, + () -> + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070"))); + } + + @Test + void zeroDurationInConfigThrows(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.json", + """ + { "url": "https://example.com", "rps": 10, "duration": 0 } + """); + + assertThrows( + ArgsException.class, + () -> + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070"))); + } + + @Test + void urlWithoutProtocolInConfigThrows(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.json", + """ + { "url": "example.com", "rps": 10, "duration": 10 } + """); + + assertThrows( + ArgsException.class, + () -> + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070"))); + } + + @Test + void configYamlProducesCorrectMasterArgs(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.yaml", + """ + url: "https://httpbin.org/get" + rps: 10 + duration: 30 + """); + + MasterArgs result = + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070")); + + TestConfig config = result.testConfig(); + assertEquals("https://httpbin.org/get", config.url()); + assertEquals(10, config.rps()); + assertEquals(30, config.duration()); + assertEquals("http://localhost:7070", result.agentUrl()); + } + + @Test + void configYmlProducesCorrectMasterArgs(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.yml", + """ + url: "https://httpbin.org/get" + rps: 10 + duration: 30 + """); + + MasterArgs result = + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070")); + + TestConfig config = result.testConfig(); + assertEquals("https://httpbin.org/get", config.url()); + assertEquals(10, config.rps()); + assertEquals(30, config.duration()); + assertEquals("http://localhost:7070", result.agentUrl()); + } + + @Test + void unsupportedConfigExtensionThrows(@TempDir Path tempDir) throws Exception { + Path configPath = + writeTempFile( + tempDir, + "config.txt", + """ + url=https://example.com + rps=10 + duration=30 + """); + + assertThrows( + ArgsException.class, + () -> + ArgsParser.parse(args("--config=" + configPath.toString(), "--agent=localhost:7070"))); + } + @Test void httpUrlIsAccepted() { assertDoesNotThrow(