mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-13 10:47:21 +00:00
Added an analyzer which keeps line breaks LF on every system (#1193)
This commit is contained in:
1 parent
b1c1f13993
commit
65d50de033
96 files changed
+741
-393
No files matched your search
@@ -0,0 +1,110 @@
|
||||
using AIStudio.Tests.Models.Generation;
|
||||
|
||||
using SourceCodeRules.UsageAnalyzers;
|
||||
using SourceCodeRules.UsageCodeFixes;
|
||||
|
||||
namespace AIStudio.Tests.Analyzers;
|
||||
|
||||
/// <summary>
|
||||
/// Checks that MWAIS0018 refuses the line break of the system and that its code fix writes LF instead.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// StringBuilder.AppendLine and Environment.NewLine write CR LF on Windows. The pipeline runs the tests on Linux
|
||||
/// only, where both write LF, so a text which differs on Windows shows up nowhere but on the computer of a developer.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class SystemLineBreakAnalyzerTests
|
||||
{
|
||||
private const string USINGS = """
|
||||
using System;
|
||||
using System.Text;
|
||||
""";
|
||||
|
||||
private const string USINGS_WITH_APPEND_LINE_LF = """
|
||||
using System;
|
||||
using System.Text;
|
||||
using SharedTools.Text;
|
||||
""";
|
||||
|
||||
[TestCase("return new StringBuilder().AppendLine().ToString();", "StringBuilder.AppendLine", TestName = "AppendLine without a text")]
|
||||
[TestCase("""return new StringBuilder().AppendLine("Text").ToString();""", "StringBuilder.AppendLine", TestName = "AppendLine with a text")]
|
||||
[TestCase("""return new StringBuilder().AppendLine($"Number {number}").ToString();""", "StringBuilder.AppendLine", TestName = "AppendLine with an interpolated text")]
|
||||
[TestCase("""return $"First{Environment.NewLine}Second";""", "Environment.NewLine", TestName = "Environment.NewLine in an interpolated text")]
|
||||
[TestCase("return string.Join(Environment.NewLine, items);", "Environment.NewLine", TestName = "Environment.NewLine as a separator")]
|
||||
[TestCase("""return "First" + System.Environment.NewLine;""", "Environment.NewLine", TestName = "Environment.NewLine with its namespace")]
|
||||
public async Task TheLineBreakOfTheSystemIsRefused(string statement, string name)
|
||||
{
|
||||
var reported = await CompilationHarness.AnalyzeAsync(ClassWith(USINGS, statement), new SystemLineBreakAnalyzer());
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(reported, Has.Count.EqualTo(1));
|
||||
Assert.That(reported.FirstOrDefault()?.Id, Is.EqualTo("MWAIS0018"));
|
||||
Assert.That(reported.FirstOrDefault()?.GetMessage(), Does.Contain($"'{name}'"));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("""return new StringBuilder().AppendLineLF("Text").ToString();""", TestName = "AppendLineLF")]
|
||||
[TestCase("""return new StringBuilder().Append("Text").Append('\n').ToString();""", TestName = "An LF of its own")]
|
||||
[TestCase("""return new Lines().AppendLine("Text");""", TestName = "A method AppendLine of another type")]
|
||||
[TestCase("return Console.Out.NewLine;", TestName = "The line break of a writer")]
|
||||
public async Task AnythingElseIsLeftAlone(string statement)
|
||||
{
|
||||
var reported = await CompilationHarness.AnalyzeAsync(ClassWith(USINGS_WITH_APPEND_LINE_LF, statement), new SystemLineBreakAnalyzer());
|
||||
|
||||
Assert.That(reported, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TheCodeFixUsesAppendLineLFWhereItIsInScope()
|
||||
{
|
||||
var source = ClassWith(USINGS_WITH_APPEND_LINE_LF, """return new StringBuilder().AppendLine("Text").ToString();""");
|
||||
var expected = ClassWith(USINGS_WITH_APPEND_LINE_LF, """return new StringBuilder().AppendLineLF("Text").ToString();""");
|
||||
|
||||
var fixedSource = await CompilationHarness.FixAsync(source, new SystemLineBreakAnalyzer(), new SystemLineBreakCodeFixProvider());
|
||||
|
||||
Assert.That(fixedSource.ReplaceLineEndings("\n"), Is.EqualTo(expected.ReplaceLineEndings("\n")), "The using directive is there already, so the fix renames the call and adds nothing.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TheCodeFixAddsTheUsingDirectiveWhereItIsMissing()
|
||||
{
|
||||
var source = ClassWith(USINGS, """return new StringBuilder().AppendLine($"Number {number}").ToString();""");
|
||||
var expected = ClassWith(USINGS_WITH_APPEND_LINE_LF, """return new StringBuilder().AppendLineLF($"Number {number}").ToString();""");
|
||||
|
||||
var fixedSource = await CompilationHarness.FixAsync(source, new SystemLineBreakAnalyzer(), new SystemLineBreakCodeFixProvider());
|
||||
|
||||
Assert.That(fixedSource.ReplaceLineEndings("\n"), Is.EqualTo(expected.ReplaceLineEndings("\n")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TheCodeFixWritesAStringWithLfForEnvironmentNewLine()
|
||||
{
|
||||
var source = ClassWith(USINGS, "return string.Join(Environment.NewLine, items);");
|
||||
var expected = ClassWith(USINGS, """return string.Join("\n", items);""");
|
||||
|
||||
var fixedSource = await CompilationHarness.FixAsync(source, new SystemLineBreakAnalyzer(), new SystemLineBreakCodeFixProvider());
|
||||
|
||||
Assert.That(fixedSource.ReplaceLineEndings("\n"), Is.EqualTo(expected.ReplaceLineEndings("\n")));
|
||||
}
|
||||
|
||||
private static string ClassWith(string usings, string statement) =>
|
||||
$$"""
|
||||
{{usings}}
|
||||
|
||||
namespace Sample;
|
||||
|
||||
public sealed class Lines
|
||||
{
|
||||
public string AppendLine(string text) => text;
|
||||
}
|
||||
|
||||
public static class Texts
|
||||
{
|
||||
public static string Build(string[] items, int number)
|
||||
{
|
||||
{{statement}}
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -141,7 +141,7 @@ public sealed class ChatThreadEditTests
|
||||
Directory.CreateDirectory(transcriptDirectory);
|
||||
|
||||
var path = Path.Combine(transcriptDirectory, $"{stem}-transcript-0001.md");
|
||||
File.WriteAllText(path, $"# Transcription: {stem}.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome.");
|
||||
File.WriteAllText(path, $"# Transcription: {stem}.mp4\n\nHello and welcome.");
|
||||
return new ManagedTranscriptAttachment(Path.GetFileName(path), path, new FileInfo(path).Length, $"{stem}.mp4", false);
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ public sealed class ContentExtensionsTests
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(exported, Does.StartWith(file.Content), "The document stays as the model wrote it.");
|
||||
Assert.That(appended.Split(Environment.NewLine), Has.All.StartWith("%"), "A line LaTeX would read could stop the whole run.");
|
||||
Assert.That(appended.Split('\n'), Has.All.StartWith("%"), "A line LaTeX would read could stop the whole run.");
|
||||
Assert.That(appended, Does.Contain(TOOL_SOURCE.URL));
|
||||
}
|
||||
}
|
||||
@@ -347,5 +347,5 @@ public sealed class ContentExtensionsTests
|
||||
.Select(block => block is HeadingBlock heading ? $"h{heading.Level}" : block.GetType().Name)
|
||||
.ToList();
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
private static string Lines(params string[] lines) => string.Join('\n', lines);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ public sealed class MarkdownRenderPlannerTests
|
||||
{
|
||||
var parts = Parts("<html><body><p>Hi</p></body></html>");
|
||||
|
||||
Assert.That(parts, Is.EqualTo(new[] { $"MARKDOWN: ```html{Environment.NewLine}<html><body><p>Hi</p></body></html>{Environment.NewLine}```" }));
|
||||
Assert.That(parts, Is.EqualTo(new[] { "MARKDOWN: ```html\n<html><body><p>Hi</p></body></html>\n```" }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -92,5 +92,5 @@ public sealed class ExportableMarkdownTests
|
||||
|
||||
private static MarkdownDocument Render(string answer) => Markdig.Markdown.Parse(answer, Markdown.CHAT_MARKDOWN_PIPELINE);
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
private static string Lines(params string[] lines) => string.Join('\n', lines);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public sealed class CapabilityCharacterizationTests
|
||||
}
|
||||
|
||||
File.WriteAllText(CapabilitySnapshot.ACTUAL_FILE_PATH, current);
|
||||
Assert.Fail($"The capabilities of {DescribeDifference(recorded, current)}{Environment.NewLine}{Environment.NewLine}The full result was written to {CapabilitySnapshot.ACTUAL_FILE_PATH}.");
|
||||
Assert.Fail($"The capabilities of {DescribeDifference(recorded, current)}\n\nThe full result was written to {CapabilitySnapshot.ACTUAL_FILE_PATH}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,19 +76,19 @@ public sealed class CapabilityCharacterizationTests
|
||||
var added = currentLines.Keys.Except(recordedLines.Keys).ToList();
|
||||
var removed = recordedLines.Keys.Except(currentLines.Keys).ToList();
|
||||
|
||||
var message = new StringBuilder($"{changed.Count} model(s) changed, {added.Count} came into the corpus, {removed.Count} left it:").Append(Environment.NewLine);
|
||||
var message = new StringBuilder($"{changed.Count} model(s) changed, {added.Count} came into the corpus, {removed.Count} left it:").Append('\n');
|
||||
foreach (var model in changed.Take(LINES_SHOWN))
|
||||
{
|
||||
message.Append(Environment.NewLine).Append(" ").Append(model);
|
||||
message.Append(Environment.NewLine).Append(" was: ").Append(recordedLines[model]);
|
||||
message.Append(Environment.NewLine).Append(" now: ").Append(currentLines[model]);
|
||||
message.Append('\n').Append(" ").Append(model);
|
||||
message.Append('\n').Append(" was: ").Append(recordedLines[model]);
|
||||
message.Append('\n').Append(" now: ").Append(currentLines[model]);
|
||||
}
|
||||
|
||||
foreach (var model in added.Take(LINES_SHOWN))
|
||||
message.Append(Environment.NewLine).Append(" + ").Append(model).Append(": ").Append(currentLines[model]);
|
||||
message.Append('\n').Append(" + ").Append(model).Append(": ").Append(currentLines[model]);
|
||||
|
||||
foreach (var model in removed.Take(LINES_SHOWN))
|
||||
message.Append(Environment.NewLine).Append(" - ").Append(model).Append(": ").Append(recordedLines[model]);
|
||||
message.Append('\n').Append(" - ").Append(model).Append(": ").Append(recordedLines[model]);
|
||||
|
||||
return message.ToString();
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public static class CompilationHarness
|
||||
.Where(diagnostic => diagnostic.Severity is DiagnosticSeverity.Error)
|
||||
.Select(diagnostic => diagnostic.ToString());
|
||||
|
||||
return string.Join(Environment.NewLine, errors);
|
||||
return string.Join('\n', errors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,6 +2,8 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
using SharedTools.Text;
|
||||
|
||||
namespace AIStudio.Tests.Serialization;
|
||||
|
||||
/// <summary>
|
||||
@@ -30,20 +32,20 @@ public static class JsonContractDescription
|
||||
: $"converter {ConverterName(typeInfo.Converter)}, created {(typeInfo.CreateObject is null ? "otherwise" : "without arguments")}";
|
||||
|
||||
var text = new StringBuilder();
|
||||
text.AppendLine($"{TypeName(typeInfo.Type)}: {typeInfo.Kind}, {creation}");
|
||||
text.AppendLine($" handling: numbers {typeInfo.NumberHandling}, unmapped members {typeInfo.UnmappedMemberHandling}, object creation {typeInfo.PreferredPropertyObjectCreationHandling}");
|
||||
text.AppendLine($" callbacks: serializing {typeInfo.OnSerializing is not null}, serialized {typeInfo.OnSerialized is not null}, deserializing {typeInfo.OnDeserializing is not null}, deserialized {typeInfo.OnDeserialized is not null}");
|
||||
text.AppendLineLF($"{TypeName(typeInfo.Type)}: {typeInfo.Kind}, {creation}");
|
||||
text.AppendLineLF($" handling: numbers {typeInfo.NumberHandling}, unmapped members {typeInfo.UnmappedMemberHandling}, object creation {typeInfo.PreferredPropertyObjectCreationHandling}");
|
||||
text.AppendLineLF($" callbacks: serializing {typeInfo.OnSerializing is not null}, serialized {typeInfo.OnSerialized is not null}, deserializing {typeInfo.OnDeserializing is not null}, deserialized {typeInfo.OnDeserialized is not null}");
|
||||
if (typeInfo.KeyType is { } keyType)
|
||||
text.AppendLine($" key: {TypeName(keyType)}");
|
||||
text.AppendLineLF($" key: {TypeName(keyType)}");
|
||||
|
||||
if (typeInfo.ElementType is { } elementType)
|
||||
text.AppendLine($" element: {TypeName(elementType)}");
|
||||
text.AppendLineLF($" element: {TypeName(elementType)}");
|
||||
|
||||
if (typeInfo.PolymorphismOptions is { } polymorphism)
|
||||
{
|
||||
text.AppendLine($" polymorphism: discriminator {polymorphism.TypeDiscriminatorPropertyName}, unrecognized ignored {polymorphism.IgnoreUnrecognizedTypeDiscriminators}, unknown types {polymorphism.UnknownDerivedTypeHandling}");
|
||||
text.AppendLineLF($" polymorphism: discriminator {polymorphism.TypeDiscriminatorPropertyName}, unrecognized ignored {polymorphism.IgnoreUnrecognizedTypeDiscriminators}, unknown types {polymorphism.UnknownDerivedTypeHandling}");
|
||||
foreach (var derivedType in polymorphism.DerivedTypes)
|
||||
text.AppendLine($" derived: {TypeName(derivedType.DerivedType)} as {derivedType.TypeDiscriminator}");
|
||||
text.AppendLineLF($" derived: {TypeName(derivedType.DerivedType)} as {derivedType.TypeDiscriminator}");
|
||||
}
|
||||
|
||||
foreach (var property in typeInfo.Properties)
|
||||
@@ -53,7 +55,7 @@ public static class JsonContractDescription
|
||||
//
|
||||
if (IsIgnored(property))
|
||||
{
|
||||
text.AppendLine($" property {property.Name}: ignored");
|
||||
text.AppendLineLF($" property {property.Name}: ignored");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -61,7 +63,7 @@ public static class JsonContractDescription
|
||||
?$"set {property.Set is not null || property.AssociatedParameter is not null}"
|
||||
: $"set {property.Set is not null}, constructor parameter {property.AssociatedParameter?.Name ?? "none"}";
|
||||
|
||||
text.AppendLine($" property {property.Name}: {TypeName(property.PropertyType)}, order {property.Order}, get {property.Get is not null}, {setting}, required {property.IsRequired}, extension data {property.IsExtensionData}, conditional {property.ShouldSerialize is not null}, converter {ConverterName(property.CustomConverter)}, nullable get {property.IsGetNullable}, nullable set {property.IsSetNullable}, numbers {property.NumberHandling}, object creation {property.ObjectCreationHandling}");
|
||||
text.AppendLineLF($" property {property.Name}: {TypeName(property.PropertyType)}, order {property.Order}, get {property.Get is not null}, {setting}, required {property.IsRequired}, extension data {property.IsExtensionData}, conditional {property.ShouldSerialize is not null}, converter {ConverterName(property.CustomConverter)}, nullable get {property.IsGetNullable}, nullable set {property.IsSetNullable}, numbers {property.NumberHandling}, object creation {property.ObjectCreationHandling}");
|
||||
}
|
||||
|
||||
return text.ToString();
|
||||
|
||||
@@ -77,7 +77,7 @@ public sealed class UserProvidedAPIKeyOptionsTests
|
||||
|
||||
var descriptor = PluginConfigurationObjectDescriptors.Get(type);
|
||||
var section = descriptor.LuaTableName;
|
||||
var optionLines = string.Join(Environment.NewLine, options.Select(option => $"CONFIG[\"{section}\"][1]{option}"));
|
||||
var optionLines = string.Join('\n', options.Select(option => $"CONFIG[\"{section}\"][1]{option}"));
|
||||
|
||||
var state = LuaState.Create();
|
||||
state.OpenBasicLibrary();
|
||||
|
||||
+14
-5
@@ -37,15 +37,24 @@
|
||||
<ProjectReference Include="..\MindWork AI Studio\MindWork AI Studio.csproj" />
|
||||
|
||||
<!--
|
||||
Referenced as plain libraries, not as analyzers: the tests run the generator and the
|
||||
analyzer themselves, against a snippet they compile in memory. Building them into this
|
||||
project as analyzers would have them judge the test code instead, which is not what is
|
||||
being checked here.
|
||||
Referenced as libraries: the tests run the generator and the analyzers themselves, against
|
||||
a snippet they compile in memory. SourceCodeRules is an analyzer of this project as well,
|
||||
but only for MWAIS0018, see below: a test which expects the line break of the system fails
|
||||
on Windows only, where no pipeline runs the tests. The other rules are written for the code
|
||||
of the app and would judge test code which has its reasons, such as new Random() with a seed.
|
||||
-->
|
||||
<ProjectReference Include="..\SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj" />
|
||||
<ProjectReference Include="..\SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj" OutputItemType="Analyzer" />
|
||||
<ProjectReference Include="..\SourceGeneratedMappings\SourceGeneratedMappings.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Every rule of SourceCodeRules except MWAIS0018, the line break of the system. A new rule judges
|
||||
the tests, too, until it is listed here.
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);MWAIS0001;MWAIS0002;MWAIS0003;MWAIS0004;MWAIS0005;MWAIS0006;MWAIS0007;MWAIS0008;MWAIS0009;MWAIS0010;MWAIS0011;MWAIS0012;MWAIS0013;MWAIS0014;MWAIS0015;MWAIS0016;MWAIS0017</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Compiled here as well, so that the tests check the same options of the app which the trim smoke test finds.
|
||||
-->
|
||||
|
||||
@@ -78,5 +78,5 @@ public sealed class AIGeneratedContentDisclosureServiceTests
|
||||
|
||||
private static SettingsManager CreateSettingsManager() => new(NullLogger<SettingsManager>.Instance, null!);
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
private static string Lines(params string[] lines) => string.Join('\n', lines);
|
||||
}
|
||||
@@ -99,8 +99,8 @@ public sealed class ClassicRagPassagesTests
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(augmentation.UsedContexts.Select(context => context.Context), Is.EqualTo(new IRetrievalContext[] { handbook, manual }), "Without the check, the AI receives every context, in the order it was retrieved.");
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain($"# Retrieval context 1 of 2{Environment.NewLine}"));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain($"# Retrieval context 2 of 2{Environment.NewLine}"));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain("# Retrieval context 1 of 2\n"));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain("# Retrieval context 2 of 2\n"));
|
||||
Assert.That(this.runtime.FilteredBatches, Has.Count.EqualTo(1), "All contexts are filtered together, in one request.");
|
||||
Assert.That(this.runtime.FilteredTexts, Has.Count.EqualTo(4), "Each description and each matched text is filtered once, and nothing a second time on its own.");
|
||||
}
|
||||
@@ -124,9 +124,9 @@ public sealed class ClassicRagPassagesTests
|
||||
{
|
||||
Assert.That(augmentation.UsedContexts.Select(context => context.Context), Is.EqualTo(new IRetrievalContext[] { handbookIntroduction, manual }), "The best passage of each data source fits; the second best of the handbook does not.");
|
||||
Assert.That(augmentation.PassageCounts, Is.EqualTo(new[] { 1, 1 }));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain($"# Retrieval context 2 of 2{Environment.NewLine}"));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Contain("# Retrieval context 2 of 2\n"));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.Not.Contain("The results of the handbook."));
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.EndWith($"1 did not fit into this request and was left out. An answer based on the data sources may therefore be incomplete.{Environment.NewLine}"), "The model learns that it did not read everything the search found.");
|
||||
Assert.That(augmentation.ChatThread.AugmentedData, Does.EndWith("1 did not fit into this request and was left out. An answer based on the data sources may therefore be incomplete.\n"), "The model learns that it did not read everything the search found.");
|
||||
Assert.That(warnings.Messages, Has.Count.EqualTo(1));
|
||||
Assert.That(warnings.Messages.SingleOrDefault(), Does.Contain("had to do without 1 of them"), "The user learns about it with the answer.");
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ public sealed class FileExportFormatTests
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(found, Is.True);
|
||||
Assert.That(comment.Split(Environment.NewLine), Is.EqualTo(new[] { "% # Sources", "%", "% - [1] A title with 100 % and a_b" }), "LaTeX has no end of a comment, only the end of a line.");
|
||||
Assert.That(comment.Split('\n'), Is.EqualTo(new[] { "% # Sources", "%", "% - [1] A title with 100 % and a_b" }), "LaTeX has no end of a comment, only the end of a line.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,5 +177,5 @@ public sealed class FileExportFormatTests
|
||||
}
|
||||
}
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
private static string Lines(params string[] lines) => string.Join('\n', lines);
|
||||
}
|
||||
@@ -93,5 +93,5 @@ public sealed class MarkdownTests
|
||||
Assert.That(Markdown.AppendBlock(string.Empty, "**Notice**"), Is.EqualTo("**Notice**"), "With nothing above it, a blank line would only start the text with an empty line.");
|
||||
}
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
private static string Lines(params string[] lines) => string.Join('\n', lines);
|
||||
}
|
||||
@@ -268,7 +268,7 @@ public sealed class PlainFileExportTests
|
||||
"```");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(MarkdownRenderPlanner.Build(answer), ',');
|
||||
var partStart = answer.LastIndexOf("$$", StringComparison.Ordinal) + "$$".Length + Environment.NewLine.Length;
|
||||
var partStart = answer.LastIndexOf("$$", StringComparison.Ordinal) + "$$\n".Length;
|
||||
var part = Markdig.Markdown.Parse(answer[partStart..], Markdown.CHAT_MARKDOWN_PIPELINE);
|
||||
var blockOfThePart = part.Descendants<FencedCodeBlock>().Single();
|
||||
|
||||
@@ -280,7 +280,19 @@ public sealed class PlainFileExportTests
|
||||
}
|
||||
}
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
[Test]
|
||||
public void EveryRowOfATableEndsWithCrLfAsCsvAsksFor()
|
||||
{
|
||||
var files = PlainFileExport.ExtractFiles(MarkdownRenderPlanner.Build(Lines("| Quarter | Revenue |", "|---|---|", "| Q1 | 100 |")), ',');
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(files, Has.Count.EqualTo(1));
|
||||
Assert.That(files.FirstOrDefault()?.Content, Is.EqualTo("Quarter,Revenue\r\nQ1,100\r\n"), "RFC 4180 ends every row with CR LF, on every system.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join('\n', lines);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a file from the fixtures next to this test.
|
||||
|
||||
@@ -9,6 +9,8 @@ using AIStudio.Tools.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
using SharedTools.Text;
|
||||
|
||||
namespace AIStudio.Tests.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
@@ -204,7 +206,7 @@ public sealed class ManagedSettingsCoverageTests
|
||||
""");
|
||||
|
||||
foreach (var setting in ManagedConfiguration.RegisteredSettings)
|
||||
code.AppendLine($"""CONFIG["SETTINGS"]["{setting.SettingName}"] = {ToLuaValue(setting)}""");
|
||||
code.AppendLineLF($"""CONFIG["SETTINGS"]["{setting.SettingName}"] = {ToLuaValue(setting)}""");
|
||||
|
||||
var plugin = await PluginFactory.Load(null, code.ToString());
|
||||
if (plugin is not PluginConfiguration { IsValid: true } configurationPlugin)
|
||||
|
||||
@@ -89,7 +89,7 @@ public sealed class RelativeDataPathsMigrationTests
|
||||
Directory.CreateDirectory(transcriptDirectory);
|
||||
|
||||
var transcriptPath = Path.Combine(transcriptDirectory, "meeting-transcript-0001.md");
|
||||
File.WriteAllText(transcriptPath, $"# Transcription: meeting.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome.");
|
||||
File.WriteAllText(transcriptPath, $"# Transcription: meeting.mp4\n\nHello and welcome.");
|
||||
var transcript = new ManagedTranscriptAttachment("meeting-transcript-0001.md", transcriptPath, new FileInfo(transcriptPath).Length, "meeting.mp4", false);
|
||||
|
||||
var chat = new ChatThread
|
||||
|
||||
@@ -9,6 +9,8 @@ using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
using SharedTools.Text;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
@@ -87,7 +89,7 @@ public sealed class RetrievalContextPreparationTests
|
||||
"",
|
||||
];
|
||||
|
||||
Assert.That(prepared.Single().ToMarkdown(2, 5), Is.EqualTo(string.Join(Environment.NewLine, expectedLines) + Environment.NewLine), "The model reads the passages in a layout it was prompted for, so preparing them must not change it.");
|
||||
Assert.That(prepared.Single().ToMarkdown(2, 5), Is.EqualTo(string.Join('\n', expectedLines) + "\n"), "The model reads the passages in a layout it was prompted for, so preparing them must not change it.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -97,9 +99,9 @@ public sealed class RetrievalContextPreparationTests
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(prepared.ToMarkdown(3), Does.StartWith($"# Retrieval context 3{Environment.NewLine}Data source name:"), "Semantic Search numbers its passages without knowing how many fit its budget.");
|
||||
Assert.That(prepared.ToMarkdown(), Does.StartWith($"# Retrieval context{Environment.NewLine}Data source name:"), "A context standing alone has no number.");
|
||||
Assert.That(prepared.ToMarkdown(2, 5), Is.EqualTo($"# Retrieval context 2 of 5{Environment.NewLine}{prepared.Body}{Environment.NewLine}"), "Below its header, a context is its body.");
|
||||
Assert.That(prepared.ToMarkdown(3), Does.StartWith("# Retrieval context 3\nData source name:"), "Semantic Search numbers its passages without knowing how many fit its budget.");
|
||||
Assert.That(prepared.ToMarkdown(), Does.StartWith("# Retrieval context\nData source name:"), "A context standing alone has no number.");
|
||||
Assert.That(prepared.ToMarkdown(2, 5), Is.EqualTo($"# Retrieval context 2 of 5\n{prepared.Body}\n"), "Below its header, a context is its body.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,10 +121,10 @@ public sealed class RetrievalContextPreparationTests
|
||||
var prepared = (await this.PrepareAsync(image)).Single();
|
||||
|
||||
var expected = new StringBuilder();
|
||||
expected.AppendLine("# Retrieval context");
|
||||
expected.AppendLineLF("# Retrieval context");
|
||||
RetrievalContextExtensions.AppendContextDescription(expected, image);
|
||||
RetrievalContextExtensions.AppendImageMatch(expected);
|
||||
expected.AppendLine();
|
||||
expected.AppendLineLF();
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
|
||||
@@ -296,7 +296,7 @@ public sealed class SourceExtensionsTests
|
||||
/// <param name="markdown">The Markdown of the sources.</param>
|
||||
/// <returns>The entries, in the order they stand in.</returns>
|
||||
private static IReadOnlyList<string> EntriesOf(string markdown) => markdown
|
||||
.Split(Environment.NewLine, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(line => line.StartsWith("- [", StringComparison.Ordinal))
|
||||
.ToList();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
using SharedTools;
|
||||
using SharedTools.Text;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ public sealed class SearchMailsToolTests : ToolRegistryTestBase
|
||||
|
||||
using (Assert.EnterMultipleScope())
|
||||
{
|
||||
Assert.That(function.DescriptionForLLM, Does.Contain($"- id={WORK.Id}, name='Work'{Environment.NewLine}"));
|
||||
Assert.That(function.DescriptionForLLM, Does.Contain($"- id={WORK.Id}, name='Work'\n"));
|
||||
Assert.That(function.DescriptionForLLM, Does.EndWith($"- id={PRIVATE.Id}, name='Private'"));
|
||||
Assert.That(function.DescriptionForLLM, Does.Not.Contain("per page").And.Not.Contain("last page"), "How many mails a page holds depends on how long they are, so each result tells whether a further page holds more.");
|
||||
Assert.That(properties[MailToolArguments.MAILBOX_IDS_ARGUMENT]!["items"]!["enum"]!.AsArray().Select(id => id!.GetValue<string>()), Is.EqualTo(new[] { WORK.Id, PRIVATE.Id }));
|
||||
|
||||
@@ -132,7 +132,7 @@ public sealed partial class SemanticSearchToolPagingTests
|
||||
{
|
||||
var result = await this.SearchAsync([HANDBOOK], 2, Found(Passages(1, 10, LONG)));
|
||||
|
||||
Assert.That(result.TextContent, Does.StartWith($"# Retrieval context 4{Environment.NewLine}"), "The model reads the fourth passage of the search, not the first one a second time.");
|
||||
Assert.That(result.TextContent, Does.StartWith("# Retrieval context 4\n"), "The model reads the fourth passage of the search, not the first one a second time.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -230,7 +230,7 @@ public sealed partial class SemanticSearchToolPagingTests
|
||||
Assert.That(shown.Text, Is.EqualTo(passage.MatchedText), "The matched text is what the search found, so it is the last to be shortened.");
|
||||
Assert.That(shown.SurroundingContent[0], Is.EqualTo(passage.SurroundingContent[0]));
|
||||
Assert.That(shown.SurroundingContent[^1], Does.StartWith("After ").And.Length.LessThan(LENGTH), "The surrounding content is shortened from its end.");
|
||||
Assert.That(result.TextContent, Does.Contain($"Matched text content:{Environment.NewLine}").And.Contain("Surrounding text content, shortened to fit the size limit:"));
|
||||
Assert.That(result.TextContent, Does.Contain("Matched text content:\n").And.Contain("Surrounding text content, shortened to fit the size limit:"));
|
||||
Assert.That(result.TextContent.Length, Is.LessThanOrEqualTo(SemanticSearchTool.MAX_RESULT_CHARACTERS));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ public sealed class SemanticSearchToolRequestTests
|
||||
[Test]
|
||||
public void AQueryOfSeveralLinesIsRefused()
|
||||
{
|
||||
var message = Refusal(() => SemanticSearchTool.ReadRequest(Query($"travel expenses{Environment.NewLine}hotels"), OFFERED));
|
||||
var message = Refusal(() => SemanticSearchTool.ReadRequest(Query("travel expenses\nhotels"), OFFERED));
|
||||
Assert.That(message, Does.Contain("'query'").And.Contain("single line"));
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ public sealed class WorkspaceChatCopyTests
|
||||
Directory.CreateDirectory(transcriptDirectory);
|
||||
|
||||
var path = Path.Combine(transcriptDirectory, $"{stem}-transcript-0001.md");
|
||||
File.WriteAllText(path, $"# Transcription: {stem}.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome.");
|
||||
File.WriteAllText(path, $"# Transcription: {stem}.mp4\n\nHello and welcome.");
|
||||
return new ManagedTranscriptAttachment(Path.GetFileName(path), path, new FileInfo(path).Length, $"{stem}.mp4", false);
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ public sealed class WorkspaceChatMoveTests
|
||||
|
||||
var fileName = $"{stem}-transcript-{number:D4}.md";
|
||||
var path = Path.Combine(transcriptDirectory, fileName);
|
||||
File.WriteAllText(path, $"# Transcription: {stem}.mp4{Environment.NewLine}{Environment.NewLine}Hello and welcome.");
|
||||
File.WriteAllText(path, $"# Transcription: {stem}.mp4\n\nHello and welcome.");
|
||||
return new ManagedTranscriptAttachment(fileName, path, new FileInfo(path).Length, $"{stem}.mp4", false);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user