Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SIP wire-format fixtures must preserve CRLF byte-for-byte. Treating them as
# binary disables any text transformation git or editors might otherwise apply.
*.raw binary

# Java sources, properties, markdown — normal LF normalisation in repo.
*.java text eol=lf
*.xml text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf
*.properties text eol=lf
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ Thumbs.db
# Compiled class files
*.class

# Python
__pycache__/
*.pyc
*.pyo

# Package files
*.jar
*.war
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.sip.compliance.fixture;

import java.util.Optional;

/**
* Declarative expectation attached to each torture fixture.
*
* <p>Sealed so the harness can pattern-match exhaustively on the verdict
* and tests stay total. Two outcomes:</p>
*
* <ul>
* <li>{@link Accept} — the parser must succeed and produce a message
* matching the declared metadata.</li>
* <li>{@link Reject} — the parser must throw and the rejection should
* fall into the declared category.</li>
* </ul>
*/
public sealed interface FixtureExpectation
permits FixtureExpectation.Accept, FixtureExpectation.Reject {

/** Expected verdict when the parser sees this fixture. */
record Accept(
MessageKind kind,
Optional<String> method,
Optional<String> requestUri,
Optional<Integer> status,
Optional<String> reason,
String version,
int headerCount,
int bodyLength) implements FixtureExpectation { }

/** The parser must reject this fixture; the category groups the failure. */
record Reject(String category, String detail) implements FixtureExpectation { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.sip.compliance.fixture;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;

/**
* Loads torture fixtures from the test-classpath {@code /torture/} root.
*
* <h2>Layout</h2>
* <pre>
* torture/
* self-test/
* simple-options.raw
* simple-options.expect.properties
* rfc4475/
* 3.1.1.1-short-tortuous-invite.raw
* 3.1.1.1-short-tortuous-invite.expect.properties
* </pre>
*
* <p>Each {@code <id>.raw} file is a byte-exact SIP message, paired with a
* {@code <id>.expect.properties} sibling that declares the expected parse
* outcome (see {@link FixtureExpectation}).</p>
*
* <p>The repository walks the directory tree rooted at the classpath
* resource {@code torture/} — which resolves to
* {@code src/test/resources/torture/} when run from this module — and
* returns every paired fixture in deterministic id order.</p>
*/
public final class FixtureRepository {

private static final String ROOT = "torture";

private FixtureRepository() { }

/** Loads all fixtures available on the test classpath. */
public static List<TortureFixture> loadAll() {
Path root = resolveRoot();
List<TortureFixture> out = new ArrayList<>();
try (Stream<Path> stream = Files.walk(root)) {
stream
.filter(p -> p.toString().endsWith(".raw"))
.sorted(Comparator.comparing(Path::toString))
.forEach(rawPath -> out.add(load(root, rawPath)));
} catch (IOException e) {
throw new UncheckedIOException("walking " + root, e);
}
return List.copyOf(out);
}

private static Path resolveRoot() {
URL url = FixtureRepository.class.getClassLoader().getResource(ROOT);
if (url == null) {
throw new IllegalStateException("classpath root not found: " + ROOT);
}
try {
return Paths.get(url.toURI());
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}

private static TortureFixture load(Path root, Path rawPath) {
Path expectPath = sibling(rawPath, ".expect.properties");
if (!Files.exists(expectPath)) {
throw new IllegalStateException(
"missing expectation file for fixture: " + rawPath
+ " (expected at " + expectPath + ")");
}

byte[] bytes;
Properties props = new Properties();
try {
bytes = Files.readAllBytes(rawPath);
try (var in = Files.newInputStream(expectPath)) {
props.load(in);
}
} catch (IOException e) {
throw new UncheckedIOException("loading fixture " + rawPath, e);
}

Path relative = root.relativize(rawPath);
String idWithExt = relative.toString().replace('\\', '/');
String id = idWithExt.substring(0, idWithExt.length() - ".raw".length());
String source = relative.getNameCount() > 1
? relative.getName(0).toString()
: "ungrouped";

FixtureExpectation expectation = parseExpectation(id, props, bytes.length);
return new TortureFixture(id, source, bytes, expectation);
}

private static Path sibling(Path rawPath, String suffix) {
String filename = rawPath.getFileName().toString();
String base = filename.substring(0, filename.length() - ".raw".length());
return rawPath.resolveSibling(base + suffix);
}

private static FixtureExpectation parseExpectation(String id, Properties p, int rawSize) {
String verdict = required(p, "verdict", id).trim().toLowerCase();
return switch (verdict) {
case "accept" -> parseAccept(id, p, rawSize);
case "reject" -> parseReject(id, p);
default -> throw new IllegalStateException(
"fixture " + id + ": unknown verdict '" + verdict + "'");
};
}

private static FixtureExpectation.Accept parseAccept(String id, Properties p, int rawSize) {
MessageKind kind = parseKind(id, required(p, "message.type", id));
Optional<String> method = optional(p, "request.method");
Optional<String> requestUri = optional(p, "request.uri");
Optional<Integer> status = optional(p, "response.status").map(Integer::parseInt);
Optional<String> reason = optional(p, "response.reason");
String version = p.getProperty("sip.version", "SIP/2.0");
int headerCount = Integer.parseInt(required(p, "header.count", id));
int bodyLength = Integer.parseInt(p.getProperty("body.length", "0"));

if (kind == MessageKind.REQUEST && (method.isEmpty() || requestUri.isEmpty())) {
throw new IllegalStateException(
"fixture " + id + ": request fixtures must declare both "
+ "request.method and request.uri");
}
if (kind == MessageKind.RESPONSE && (status.isEmpty() || reason.isEmpty())) {
throw new IllegalStateException(
"fixture " + id + ": response fixtures must declare both "
+ "response.status and response.reason");
}
if (bodyLength < 0 || bodyLength > rawSize) {
throw new IllegalStateException(
"fixture " + id + ": body.length out of range: " + bodyLength
+ " (raw size " + rawSize + ")");
}

return new FixtureExpectation.Accept(
kind, method, requestUri, status, reason, version, headerCount, bodyLength);
}

private static FixtureExpectation.Reject parseReject(String id, Properties p) {
String category = required(p, "error.category", id);
String detail = p.getProperty("error.detail", "");
return new FixtureExpectation.Reject(category, detail);
}

private static MessageKind parseKind(String id, String raw) {
return switch (raw.trim().toLowerCase()) {
case "request" -> MessageKind.REQUEST;
case "response" -> MessageKind.RESPONSE;
default -> throw new IllegalStateException(
"fixture " + id + ": invalid message.type '" + raw + "'");
};
}

private static String required(Properties p, String key, String id) {
String v = p.getProperty(key);
if (v == null || v.isBlank()) {
throw new IllegalStateException(
"fixture " + id + ": missing required property '" + key + "'");
}
return v;
}

private static Optional<String> optional(Properties p, String key) {
String v = p.getProperty(key);
return (v == null || v.isBlank()) ? Optional.empty() : Optional.of(v);
}

/** Convenience for tests: helpful when reporting a fixture in a message. */
public static URI uriOf(String id) {
return URI.create("classpath:" + ROOT + "/" + id + ".raw");
}
}
Loading
Loading