Skip to content
Merged
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
161 changes: 161 additions & 0 deletions src/test/java/software/stephenson/trace/TraceClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -35,6 +36,7 @@
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

/**
* Drives the client against a real HTTP server on a loopback port -- the
Expand Down Expand Up @@ -1042,6 +1044,50 @@ void serverWideConfig_aQuotedFalseDoesNotDisableYet() {
}
}

@Test
void serverWideConfig_aByteThatIsNotUtf8LeavesReportingOnYet(@TempDir Path plugins) throws Exception {
// Characterizes current behaviour, which issue #40 questions: one
// byte of another encoding fails the whole read, which counts as an
// IO failure, so the enabled: false below it is never seen.
// Arrange
Path file = plugins.resolve("trace").resolve("config.yml");
Files.createDirectories(file.getParent());
Files.write(file, "# café\nenabled: false\n".getBytes(StandardCharsets.ISO_8859_1));
RecordingHandler log = new RecordingHandler();
Logger logger = Logger.getLogger("TraceClientTest.serverWideNotUtf8");
logger.setLevel(Level.ALL);
logger.addHandler(log);

// Act
TraceClient client = TraceClient.builder(baseUrl(), "MyPlugin", "1.2.3").key("k")
.serverWideConfig(plugins.toFile()).logger(logger).build();

// Assert
assertTrue(client.isEnabled(), "the switch was not read");
assertTrue(log.await(1, TimeUnit.SECONDS), "the failure should be mentioned at FINE");
assertTrue(log.records.get(0).getMessage().contains("could not read server-wide config"),
log.records.get(0).getMessage());
client.close();
}

@Test
void serverWideConfig_aByteOrderMarkHidesAFirstLineEnabledFalseYet(@TempDir Path plugins) throws Exception {
// Characterizes current behaviour, which issue #40 questions: the
// mark is not whitespace to ENABLED_LINE, so the line does not match.
// Arrange
Path file = plugins.resolve("trace").resolve("config.yml");
Files.createDirectories(file.getParent());
Files.write(file, "enabled: false\n".getBytes(StandardCharsets.UTF_8));

// Act
TraceClient client = TraceClient.builder(baseUrl(), "MyPlugin", "1.2.3").key("k")
.serverWideConfig(plugins.toFile()).build();

// Assert
assertTrue(client.isEnabled(), "the switch was not recognised");
client.close();
}

@Test
void environment_disablesAndWinsOverTheServerWideFile(@TempDir Path plugins) throws Exception {
// Arrange
Expand Down Expand Up @@ -1247,6 +1293,26 @@ void serverWideTags_doubleQuotedValuesUnescapeNewlineTabCarriageReturnAndBacksla
assertEquals(expected, tags);
}

@Test
void serverWideTags_dropKeysAndValuesWhoseQuoteIsNeverClosed() {
Map<String, String> tags = tagsOf("tags:\n"
+ " \"open-double: x\n"
+ " 'open-single: x\n"
+ " \"mismatched': x\n"
+ " \": lone-quote\n"
+ " value: \"ends in a backslash\\\n"
+ " ok: fine\n");
assertEquals(Collections.singletonMap("ok", "fine"), tags);
}

@Test
void serverWideTags_aSingleQuotedValueTakesACommentButNothingElseAfterIt() {
Map<String, String> tags = tagsOf("tags:\n"
+ " a: 'x' # comment after a quote\n"
+ " b: 'x' junk\n");
assertEquals(Collections.singletonMap("a", "x"), tags);
}

@Test
void serverWideTags_dropValuesThatOpenAnAnchorAliasTagOrFoldedScalar() {
Map<String, String> tags = tagsOf("tags:\n"
Expand Down Expand Up @@ -1576,6 +1642,40 @@ void installId_unwritableConfigFallsBackToAnInMemoryIdWithoutThrowing(@TempDir P
assertEquals("not a directory", new String(Files.readAllBytes(notADirectory), StandardCharsets.UTF_8));
}

@Test
void installId_aConfigThatCannotBeAppendedToFallsBackToAnInMemoryIdAndIsLoggedFine(@TempDir Path plugins) throws Exception {
// Arrange
// The test above fails at the read. To fail at the append, the file
// has to read fine and refuse a write, even as root: /proc/version
// does, and has no enabled: or server-id: line in it.
Path readOnly = Paths.get("/proc/version");
assumeTrue(Files.isReadable(readOnly), "needs Linux's /proc");
Path config = Files.createDirectory(plugins.resolve("trace")).resolve("config.yml");
Files.createSymbolicLink(config, readOnly);
RecordingHandler log = new RecordingHandler();
Logger logger = Logger.getLogger("TraceClientTest.serverIdAppend");
logger.setLevel(Level.ALL);
logger.addHandler(log);

// Act
TraceClient first = assertDoesNotThrow(() -> TraceClient.builder(baseUrl(), "MyPlugin", "1.2.3").key("k")
.serverWideConfig(plugins.toFile()).logger(logger).build());
TraceClient second = TraceClient.builder(baseUrl(), "MyPlugin", "1.2.3").key("k").serverWideConfig(plugins.toFile()).build();
first.report("startup");

// Assert
assertTrue(arrived.await(5, TimeUnit.SECONDS));
first.close();
second.close();
assertTrue(first.isEnabled());
assertTrue(first.installId().matches(UUID_PATTERN), first.installId());
assertNotEquals(first.installId(), second.installId(), "nothing persisted: each process gets its own");
assertTrue(received.get(0).body.contains("\"install\":\"" + first.installId() + "\""), received.get(0).body);
assertTrue(log.await(1, TimeUnit.SECONDS), "the fallback should be mentioned at FINE");
assertEquals(Level.FINE, log.records.get(0).getLevel());
assertTrue(log.records.get(0).getMessage().contains("could not write server-id"), log.records.get(0).getMessage());
}

@Test
void installId_anUnreadableConfigIsLoggedFineAndNotAppendedTo(@TempDir Path scratch) throws Exception {
// Arrange: as above, a "plugins directory" that is a regular file.
Expand Down Expand Up @@ -1839,6 +1939,36 @@ void installIdFile_unwritableYieldsAnInMemoryIdWithoutThrowing(@TempDir Path scr
"the fallback is logged at FINE");
}

@Test
void installIdFile_aWriteThatFailsYieldsAnInMemoryIdAndIsLoggedFine(@TempDir Path scratch) throws Exception {
// Arrange
// The test above fails at the read. A link into a directory that does
// not exist reads as "no such file", so the client goes on to write,
// and that fails too, even as root.
assumeTrue(File.separatorChar == '/', "needs POSIX symbolic links");
Path target = scratch.resolve("missing").resolve("trace-install-id");
Path link = Files.createSymbolicLink(scratch.resolve("trace-install-id"), target);
RecordingHandler log = new RecordingHandler();
Logger logger = Logger.getLogger("TraceClientTest.installIdFileWrite");
logger.setLevel(Level.ALL);
logger.addHandler(log);

// Act
TraceClient client = assertDoesNotThrow(() -> TraceClient.builder(baseUrl(), "MyCli", "1.2.3").key("k")
.installIdFile(link.toFile()).logger(logger).build());
client.report("startup");

// Assert
assertTrue(arrived.await(5, TimeUnit.SECONDS));
client.close();
assertTrue(client.installId().matches(UUID_PATTERN), client.installId());
assertTrue(received.get(0).body.contains("\"install\":\"" + client.installId() + "\""), received.get(0).body);
assertFalse(Files.exists(target));
assertTrue(log.await(1, TimeUnit.SECONDS), "the fallback should be mentioned at FINE");
assertEquals(Level.FINE, log.records.get(0).getLevel());
assertTrue(log.records.get(0).getMessage().contains("could not write install ID file"), log.records.get(0).getMessage());
}

@Test
void installIdFromFile_leavesAnUnreadableFileAlone(@TempDir Path scratch) throws Exception {
// A directory cannot be read as a file.
Expand Down Expand Up @@ -2019,6 +2149,37 @@ void coalesce_aSteadyFloodIsOneEventPerWindowAndAQuietWindowEnds() throws Except
assertEquals(sent, received.size(), "nothing was left to flush");
}

@Test
void coalesce_aWindowThatClosesLeavesOneOpenedLaterToItsOwnDeadline() throws Exception {
// Arrange
TraceClient client = TraceClient.builder(baseUrl(), "MyPlugin", "1.2.3").key("k")
.coalesceWindowMillis(2_000).build();
Map<String, String> early = Collections.singletonMap("name", "early");
Map<String, String> late = Collections.singletonMap("name", "late");

// Act
// Half a window apart, so the flush that closes the first finds the
// second still open.
client.report("command", null, early);
client.report("command", null, early);
Thread.sleep(1_000);
client.report("command", null, late);
client.report("command", null, late);
awaitReceived(3);

// Assert
assertEquals(3, received.size(), "two first reports and the early count");
assertTrue(received.get(2).body.contains("\"name\":\"early\"") && received.get(2).body.contains("\"coalesced\":\"1\""),
received.get(2).body);
Thread.sleep(200);
assertEquals(3, received.size(), "the late window has not closed yet");
awaitReceived(4);
assertEquals(4, received.size(), "it gets a flush of its own");
assertTrue(received.get(3).body.contains("\"name\":\"late\"") && received.get(3).body.contains("\"coalesced\":\"1\""),
received.get(3).body);
client.close();
}

@Test
void close_sendsEveryCountStillOpen() throws Exception {
// Arrange
Expand Down
Loading