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(