using System.Runtime.CompilerServices; using System.Text; using AIStudio.Provider; using AIStudio.Settings; namespace AIStudio.Tests.Models.Corpus; /// /// Renders the corpus and its capabilities as one text, and says where that text is kept. /// /// /// The snapshot is compared as text rather than parsed back into entries. A model ID may be /// anything a provider chooses to answer with, empty strings and separator characters included, and /// a parser would have to be right about all of it to be worth anything. Comparing the rendered text /// cannot be wrong about a name, and a diff of it reads the same in the test output as in the IDE. /// public static class CapabilitySnapshot { /// /// What a model with no capabilities at all is written as. /// /// /// An empty column would be an invisible statement. Providers do answer with nothing: an empty /// model ID and the "no provider" entry both do, and both are in the corpus. /// private const string NOTHING = "(nothing)"; private const string HEADER = """ # The capabilities the current rules answer with, for every model of the corpus. # # Generated. Do not edit by hand: run the SnapshotWriter test to write it anew, then read # the diff. Every line of it is a statement about a model which somebody has to agree with. # # Columns are provider, model ID as the provider reports it, and the capabilities sorted by # name. The ID stands here unchanged, so a line may well carry leading or trailing spaces. # # Models whose current answer the audit showed to be wrong are not in here. They live in # ExpectedChanges.cs, together with the answer the rebuild has to arrive at. # """; /// /// The directory this source file lives in, filled in by the compiler. /// /// /// The snapshot is read from the source tree, not from the build output. It is a file somebody /// reviews and commits, so the test has to fail against the file in the working copy rather /// than against a stale copy next to the assembly. /// private static readonly string DIRECTORY = ResolveDirectory(); /// /// Where the snapshot is kept. /// public static readonly string FILE_PATH = Path.Combine(DIRECTORY, "CapabilitySnapshot.txt"); /// /// Where a mismatching snapshot is written for comparison in the IDE. /// public static readonly string ACTUAL_FILE_PATH = Path.Combine(DIRECTORY, "CapabilitySnapshot.actual.txt"); /// /// Renders the given entries and the capabilities the current rules answer with. /// /// /// The text ends with the last model rather than with a line break, which is how this repository /// keeps its files. A generator disagreeing with that by one byte makes the test fail the next /// time an editor tidies the file up, and the failure says that nothing changed -- which is both /// true and useless. /// /// The entries to render. /// The snapshot text, without a trailing newline and without carriage returns. public static string Render(IEnumerable entries) { var lines = entries .OrderBy(entry => entry.Provider.ToString(), StringComparer.Ordinal) .ThenBy(entry => entry.ModelId, StringComparer.Ordinal) .Select(entry => $"{entry.Provider} | {entry.ModelId} | {Describe(AskTheCurrentRules(entry))}"); return new StringBuilder(HEADER).AppendJoin('\n', lines).ToString(); } /// /// Asks the current capability rules about one corpus entry. /// /// The entry to ask about. /// The capabilities the current rules answer with. public static List AskTheCurrentRules(CorpusEntry entry) => entry.Provider.GetModelCapabilities(new Model(entry.ModelId, null)); /// /// Writes capabilities the way a snapshot line does. /// /// /// Sorted by name, and duplicates are kept rather than folded away: a capability appearing twice /// is something to see, not something to hide. /// /// The capabilities to write. /// The capability names, or a marker when there are none. public static string Describe(IEnumerable capabilities) { var names = capabilities.Select(capability => capability.ToString()).Order(StringComparer.Ordinal).ToList(); return names.Count is 0 ? NOTHING : string.Join(", ", names); } /// /// Reads the snapshot as it stands in the source tree. /// /// The snapshot text with its line endings normalized, or null when there is none yet. public static string? Read() => File.Exists(FILE_PATH) ? File.ReadAllText(FILE_PATH).Replace("\r\n", "\n") : null; private static string ResolveDirectory([CallerFilePath] string sourceFilePath = "") => Path.GetDirectoryName(sourceFilePath)!; }