using System; using System.IO; using AcDream.Core.Chat; namespace AcDream.Core.Tests.Chat; /// /// Retail's @log file behaviour: append, never rotate, never truncate. /// public sealed class ChatSessionLogTests : IDisposable { private readonly string _directory = Path.Combine(Path.GetTempPath(), "acdream-chatlog-" + Guid.NewGuid().ToString("N")); public void Dispose() { try { Directory.Delete(_directory, recursive: true); } catch (IOException) { /* the test already told us what it needed to */ } } [Theory] [InlineData("aclog", "aclog.txt")] [InlineData("aclog.txt", "aclog.txt")] [InlineData("aclog.log", "aclog.log")] // A name that already carries ANY extension is left alone; retail tests // the extension for emptiness, not for ".txt". [InlineData("chat.old", "chat.old")] public void AnExtensionlessNameGainsDotTxt(string given, string expected) => Assert.Equal(expected, ChatSessionLog.EnsureExtension(given)); [Fact] public void LinesLandInTheFileWithTheirTimestampAndANewline() { using var log = new ChatSessionLog(_directory); Assert.True(log.Open("session", out string resolved)); Assert.Equal("session.txt", resolved); log.Write("13:05:09 ", "Dww tells you, \"hello\""); log.Write(null, "Welcome to Dereth."); log.Close(); Assert.Equal( "13:05:09 Dww tells you, \"hello\"\nWelcome to Dereth.\n", File.ReadAllText(Path.Combine(_directory, "session.txt"))); } [Fact] public void ReopeningTheSameNameAppendsRatherThanTruncating() { // Retail's own help promises this: "If this file already exists, it // will add the additional text to the end of it." Truncating would // destroy the previous session's log the moment you start a new one. using var log = new ChatSessionLog(_directory); log.Open("session", out _); log.Write(null, "first"); log.Close(); log.Open("session", out _); log.Write(null, "second"); log.Close(); Assert.Equal( "first\nsecond\n", File.ReadAllText(Path.Combine(_directory, "session.txt"))); } [Fact] public void OpeningASecondLogClosesTheFirst() { // StartCopyOutputToFile calls CloseLogFile before it does anything // else, so two files can never be open at once. using var log = new ChatSessionLog(_directory); log.Open("one", out _); log.Write(null, "to one"); Assert.True(log.Open("two", out _)); log.Write(null, "to two"); log.Close(); Assert.Equal("to one\n", File.ReadAllText(Path.Combine(_directory, "one.txt"))); Assert.Equal("to two\n", File.ReadAllText(Path.Combine(_directory, "two.txt"))); } [Fact] public void CloseReportsWhetherOneWasOpen() { // The caller picks between two different retail replies on this, so // it is load-bearing rather than informational. using var log = new ChatSessionLog(_directory); Assert.False(log.Close()); log.Open("session", out _); Assert.True(log.Close()); Assert.False(log.Close()); } [Fact] public void WritingWithNoLogOpenIsANoOp() { // The caller hands over every transcript line unconditionally, the way // retail does, so the closed case has to be silent rather than throw. using var log = new ChatSessionLog(_directory); log.Write("13:05:09 ", "nobody is listening"); Assert.False(log.IsOpen); Assert.Null(log.CurrentName); Assert.False(Directory.Exists(_directory)); } [Fact] public void AnUnopenableNameReportsFailureInsteadOfThrowing() { // Retail tells the player "Failed to redirect to file %s!" rather than // dying, so a bad name is an ordinary answer here. using var log = new ChatSessionLog(_directory); // A directory cannot be opened as a file. Directory.CreateDirectory(Path.Combine(_directory, "taken.txt")); Assert.False(log.Open("taken.txt", out string resolved)); Assert.Equal("taken.txt", resolved); Assert.False(log.IsOpen); } [Fact] public void ARootedNameIsHonouredVerbatim() { // Retail's fopen takes the string as given; a player who types a full // path means it. using var log = new ChatSessionLog(_directory); string rooted = Path.Combine(_directory, "nested", "elsewhere.txt"); Assert.True(log.Open(rooted, out string resolved)); Assert.Equal(rooted, resolved); log.Write(null, "here"); log.Close(); Assert.Equal("here\n", File.ReadAllText(rooted)); } }