Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
4 changes: 4 additions & 0 deletions volta-master/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
67 changes: 58 additions & 9 deletions volta-master/src/main/java/com/volta/master/cli/ArgsParser.java
Original file line number Diff line number Diff line change
@@ -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=<url> --rps=<rps> --duration=<seconds> --agent=<host:port>
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=<url> --rps=<rps> --duration=<seconds> --agent=<host:port>
java -jar volta-master.jar --config=./<config.json(.yaml/.yml)> --agent=<host:port>

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) {
Expand Down
165 changes: 164 additions & 1 deletion volta-master/src/test/java/com/volta/master/cli/ArgsParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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(
Expand Down
Loading