mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 04:07:21 +00:00
Added saving code blocks of every programming language (#1117)
This commit is contained in:
1 parent
1a23479f33
commit
03abd78a5f
25 files changed
+1209
-65
No files matched your search
@@ -229,6 +229,43 @@ public sealed class IContentExtensionsTests
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CodeCarriesNoSources()
|
||||
{
|
||||
var content = TextWith(Lines(
|
||||
"```python",
|
||||
"import sys",
|
||||
"print(sys.argv)",
|
||||
"```"), TOOL_SOURCE);
|
||||
|
||||
var file = FilesOf(content).Single();
|
||||
|
||||
Assert.That(content.ToExportContent(file), Is.EqualTo("import sys\nprint(sys.argv)\n"), "A program is meant for its interpreter; the only thing we add is the line break its last line lacks.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CodeWhichEndsWithALineBreakGetsNoSecondOne()
|
||||
{
|
||||
var file = new MessageFile(1, Position: 0, string.Empty, FileExportFormat.SOURCE_CODE, SourceCodeLanguages.FromCodeFenceLanguage("python"), "print(42)\n");
|
||||
|
||||
Assert.That(TextWith(string.Empty).ToExportContent(file), Is.EqualTo("print(42)\n"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ABatchFileEndsItsLinesTheWindowsWay()
|
||||
{
|
||||
var content = TextWith(Lines(
|
||||
"```bat",
|
||||
"@echo off",
|
||||
":again",
|
||||
"goto again",
|
||||
"```"));
|
||||
|
||||
var file = FilesOf(content).Single();
|
||||
|
||||
Assert.That(content.ToExportContent(file), Is.EqualTo("@echo off\r\n:again\r\ngoto again\r\n"), "The command prompt misses jump labels in a file whose lines end with LF alone.");
|
||||
}
|
||||
|
||||
[TestCase("markdown")]
|
||||
[TestCase("html")]
|
||||
[TestCase("latex")]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Markdig.Extensions.Mathematics;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace AIStudio.Tests.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which code blocks of an answer get actions, such as a button to save them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A block finds its file by where it starts. The chat renders the answer with a pipeline of its
|
||||
/// own, so the blocks are read the same way here; how an answer cut around a formula keeps its
|
||||
/// positions is checked in PlainFileExportTests.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class ExportableMarkdownTests
|
||||
{
|
||||
[Test]
|
||||
public void EachCodeBlockFindsItsFile()
|
||||
{
|
||||
var answer = Lines(
|
||||
"| Quarter | Revenue |",
|
||||
"|---|---|",
|
||||
"| Q1 | 100 |",
|
||||
string.Empty,
|
||||
"```python",
|
||||
"print(100)",
|
||||
"```",
|
||||
string.Empty,
|
||||
"```html",
|
||||
"<p>100</p>",
|
||||
"```");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(answer, ',');
|
||||
var foundFiles = Render(answer).Descendants<FencedCodeBlock>().Select(block => ExportableMarkdown.FileAt(files, block.Span.Start));
|
||||
|
||||
Assert.That(foundFiles, Is.EqualTo(new[] { files[1], files[2] }), "The table comes first among the files, but it is no code block.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFormulaFindsNoFile()
|
||||
{
|
||||
var answer = Lines("1. The energy follows from", string.Empty, " $$", " E = mc^2", " $$");
|
||||
|
||||
var formula = Render(answer).Descendants<MathBlock>().Single();
|
||||
|
||||
Assert.That(ExportableMarkdown.FileAt(PlainFileExport.ExtractFiles(answer, ','), formula.Span.Start), Is.Null, "Markdig reads a formula as a fenced block, but nobody saves a formula as a text file.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ACodeBlockTheModelNeverClosedFindsNoFile()
|
||||
{
|
||||
var answer = Lines("The answer starts normally.", string.Empty, "```python", "def main():");
|
||||
|
||||
var block = Render(answer).Descendants<FencedCodeBlock>().Single();
|
||||
|
||||
Assert.That(ExportableMarkdown.FileAt(PlainFileExport.ExtractFiles(answer, ','), block.Span.Start), Is.Null, "The file would end wherever the answer broke off.");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Lua;
|
||||
using Lua.Standard;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace AIStudio.Tests.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which format of the result files an organization can preselect for the Batch Processing
|
||||
/// assistant through a configuration plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The settings are reached through Program.SERVICE_PROVIDER, as in the app, because a managed setting
|
||||
/// writes its value and its lock there. The plugin passes the check of DataBatchProcessing, as
|
||||
/// PluginConfiguration does.
|
||||
/// </remarks>
|
||||
[TestFixture]
|
||||
public sealed class BatchProcessingResultFileFormatTests
|
||||
{
|
||||
private static readonly Guid PLUGIN_ID = Guid.Parse("9d2f6c41-3b8e-4a07-b5d1-e6a8c2f04b73");
|
||||
|
||||
private SettingsManager settingsManager = null!;
|
||||
private ServiceProvider serviceProvider = null!;
|
||||
private IServiceProvider previousServiceProvider = null!;
|
||||
|
||||
[SetUp]
|
||||
public void CreateSettings()
|
||||
{
|
||||
// Creating the settings data registers every managed setting:
|
||||
this.settingsManager = new SettingsManager(NullLogger<SettingsManager>.Instance, null!);
|
||||
|
||||
this.previousServiceProvider = Program.SERVICE_PROVIDER;
|
||||
this.serviceProvider = new ServiceCollection().AddSingleton(this.settingsManager).BuildServiceProvider();
|
||||
Program.SERVICE_PROVIDER = this.serviceProvider;
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void RestoreApplicationState()
|
||||
{
|
||||
// The managed state of a setting outlives this fixture, so we release what the plugin took over:
|
||||
ManagedConfiguration.CleanupLeftOverManagedConfigurations([], new HashSet<Guid>());
|
||||
|
||||
Program.SERVICE_PROVIDER = this.previousServiceProvider;
|
||||
this.serviceProvider.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task APluginPreselectsAFormatForEntireAnswers()
|
||||
{
|
||||
var processed = await ProcessAsync("MICROSOFT_WORD");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.True);
|
||||
Assert.That(this.ResultFileFormat, Is.EqualTo(FileExportFormat.MICROSOFT_WORD));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("CSV", TestName = "A table, which holds one table out of an answer")]
|
||||
[TestCase("SOURCE_CODE", TestName = "Code, which takes its extension from a language")]
|
||||
[TestCase("NONE", TestName = "No format at all")]
|
||||
public async Task AFormatWhichHoldsNoEntireAnswerIsRejected(string configuredFormat)
|
||||
{
|
||||
this.ResultFileFormat = FileExportFormat.HTML;
|
||||
var processed = await ProcessAsync(configuredFormat);
|
||||
|
||||
ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ResultFileFormat, out var configMeta);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(processed, Is.False, "The result files would end up without an extension or as something other than an answer.");
|
||||
Assert.That(this.ResultFileFormat, Is.EqualTo(FileExportFormat.HTML), "A rejected value leaves the choice of the user untouched.");
|
||||
Assert.That(configMeta.IsLocked, Is.False, "A rejected value locks nothing.");
|
||||
});
|
||||
}
|
||||
|
||||
private FileExportFormat ResultFileFormat
|
||||
{
|
||||
get => this.settingsManager.ConfigurationData.BatchProcessing.ResultFileFormat;
|
||||
set => this.settingsManager.ConfigurationData.BatchProcessing.ResultFileFormat = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lets a configuration plugin preselect the format of the result files.
|
||||
/// </summary>
|
||||
/// <param name="format">The name of the format as the plugin writes it.</param>
|
||||
/// <returns>Whether the plugin's value was taken.</returns>
|
||||
private static async Task<bool> ProcessAsync(string format)
|
||||
{
|
||||
var state = LuaState.Create();
|
||||
state.OpenBasicLibrary();
|
||||
|
||||
await state.DoStringAsync($"""
|
||||
SETTINGS = {"{}"}
|
||||
SETTINGS["DataBatchProcessing.ResultFileFormat"] = "{format}"
|
||||
""");
|
||||
|
||||
if (!state.Environment["SETTINGS"].TryRead<LuaTable>(out var settingsTable))
|
||||
throw new InvalidOperationException("The settings of this test are not a Lua table.");
|
||||
|
||||
return ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.ResultFileFormat, PLUGIN_ID, settingsTable, false, validator: DataBatchProcessing.IsValidResultFileFormat);
|
||||
}
|
||||
}
|
||||
@@ -101,10 +101,22 @@ public sealed class FileExportFormatTests
|
||||
Assert.That(FileExportFormat.MARKDOWN.IsPlainText(), Is.True);
|
||||
Assert.That(FileExportFormat.CSV.IsPlainText(), Is.True);
|
||||
Assert.That(FileExportFormat.TSV.IsPlainText(), Is.True);
|
||||
Assert.That(FileExportFormat.SOURCE_CODE.IsPlainText(), Is.True, "Code is text, whatever its language.");
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("css", TestName = "A language AI Studio writes no file for")]
|
||||
[Test]
|
||||
public void CodeLeavesItsFileToItsLanguage()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(FileExportFormat.SOURCE_CODE.ToFileExtension(), Is.Empty, "Python and Rust share the format, but not the extension.");
|
||||
Assert.That(FileExportFormat.SOURCE_CODE.ToFileTypeFilter(), Is.Null, "Without the language, the save dialog would offer the wrong files.");
|
||||
Assert.That(FileExportFormatExtensions.ANSWER_FORMATS, Does.Not.Contain(FileExportFormat.SOURCE_CODE), "Code is one block out of an answer, never the answer itself.");
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("css", TestName = "A language without a format of its own")]
|
||||
[TestCase("docx", TestName = "A format no code block can hold")]
|
||||
[TestCase("", TestName = "A fence without a language")]
|
||||
[TestCase(null, TestName = "A fence Markdig read no language for")]
|
||||
|
||||
@@ -2,6 +2,8 @@ using System.Runtime.CompilerServices;
|
||||
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
@@ -70,17 +72,39 @@ public sealed class PlainFileExportTests
|
||||
Assert.That(files.Select(file => file.Format), Is.EqualTo(new[] { FileExportFormat.LATEX }));
|
||||
}
|
||||
|
||||
[TestCase("```css", TestName = "A language AI Studio writes no file for")]
|
||||
[TestCase("```", TestName = "A fence without a language")]
|
||||
public void AnyOtherCodeBlockIsNotOffered(string openingFence)
|
||||
[TestCase("```python", "Python (.py)")]
|
||||
[TestCase("```rust", "Rust (.rs)")]
|
||||
[TestCase("```css", "CSS (.css)")]
|
||||
[TestCase("~~~bash", "Shell (.sh)")]
|
||||
[TestCase("```foo", "foo (.txt)", Description = "A language AI Studio does not know still names the block.")]
|
||||
[TestCase("```", "Text (.txt)", Description = "A block without a language is plain text.")]
|
||||
public void ACodeBlockInAnyOtherLanguageIsOfferedAsCode(string openingFence, string expectedName)
|
||||
{
|
||||
var files = PlainFileExport.ExtractFiles(Lines(openingFence, "body { margin: 0; }", "```"), ',');
|
||||
var files = PlainFileExport.ExtractFiles(Lines(openingFence, "print(42)", openingFence[..3]), ',');
|
||||
|
||||
Assert.That(files, Is.Empty);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(files, Has.Count.EqualTo(1));
|
||||
Assert.That(files[0].Format, Is.EqualTo(FileExportFormat.SOURCE_CODE));
|
||||
Assert.That(files[0].ToName(), Is.EqualTo(expectedName));
|
||||
Assert.That(files[0].Content, Is.EqualTo("print(42)"), "The code leaves the answer exactly as the model wrote it, without the fence around it.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFormulaIsNoCodeBlock()
|
||||
{
|
||||
var answer = Lines("The energy follows from", string.Empty, "$$", "E = mc^2", "$$", string.Empty, "for every body at rest.");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(answer, ',');
|
||||
|
||||
Assert.That(files, Is.Empty, "Markdig reads a formula as a fenced block, but nobody saves a formula as a text file.");
|
||||
}
|
||||
|
||||
[TestCase("```html", "<html><body><p>The answer broke off here", TestName = "Half a web page")]
|
||||
[TestCase("```csv", "Quarter,Revenue", TestName = "Half a table")]
|
||||
[TestCase("```python", "def main():", TestName = "Half a program")]
|
||||
[TestCase("```", "The answer broke off here", TestName = "Half a text")]
|
||||
public void ACodeBlockTheModelNeverClosedIsNotOffered(string openingFence, string content)
|
||||
{
|
||||
var files = PlainFileExport.ExtractFiles(Lines("The answer starts normally.", string.Empty, openingFence, content), ',');
|
||||
@@ -106,6 +130,12 @@ public sealed class PlainFileExportTests
|
||||
string.Empty,
|
||||
"```latex",
|
||||
@"\section{Second block}",
|
||||
"```",
|
||||
string.Empty,
|
||||
"# Script",
|
||||
string.Empty,
|
||||
"```python",
|
||||
"print('Third block')",
|
||||
"```");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(answer, ',');
|
||||
@@ -115,7 +145,84 @@ public sealed class PlainFileExportTests
|
||||
(1, "Revenue", FileExportFormat.CSV),
|
||||
(1, "Landing page", FileExportFormat.HTML),
|
||||
(2, "Landing page", FileExportFormat.LATEX),
|
||||
}), "The first code block is code block 1, even though a table stands before it.");
|
||||
(3, "Script", FileExportFormat.SOURCE_CODE),
|
||||
}), "The first code block is code block 1, even though a table stands before it, and code counts along with the other blocks.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EachFileKnowsWhereItStartsInTheAnswer()
|
||||
{
|
||||
var answer = Lines(
|
||||
"Here is the revenue:",
|
||||
string.Empty,
|
||||
"| Quarter | Revenue |",
|
||||
"|---|---|",
|
||||
"| Q1 | 100 |",
|
||||
string.Empty,
|
||||
"And the script which made it:",
|
||||
string.Empty,
|
||||
"```python",
|
||||
"print(100)",
|
||||
"```");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(answer, ',');
|
||||
|
||||
Assert.That(files.Select(file => file.Position), Is.EqualTo(new[] { answer.IndexOf("| Quarter", StringComparison.Ordinal), answer.IndexOf("```python", StringComparison.Ordinal) }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ACodeBlockInAListKnowsWhereItStarts()
|
||||
{
|
||||
var answer = Lines(
|
||||
"1. Install the package:",
|
||||
string.Empty,
|
||||
" ```bash",
|
||||
" pip install requests",
|
||||
" ```",
|
||||
"2. Run your script.");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(answer, ',');
|
||||
|
||||
Assert.That(files.Select(file => file.Position), Is.EqualTo(new[] { answer.IndexOf("```bash", StringComparison.Ordinal) }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks what a code block the chat renders relies on to find its file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The chat renders an answer in parts, cut around each formula, and Markdig reads every part on
|
||||
/// its own. A block of such a part therefore knows where it starts within its part only. Added to
|
||||
/// where the part starts in the answer, that has to be the position the file was read with.
|
||||
/// </remarks>
|
||||
[Test]
|
||||
public void ACodeBlockAfterAFormulaFindsItsFile()
|
||||
{
|
||||
var answer = Lines(
|
||||
"# Script",
|
||||
string.Empty,
|
||||
"```python",
|
||||
"print(1)",
|
||||
"```",
|
||||
string.Empty,
|
||||
"$$",
|
||||
"E = mc^2",
|
||||
"$$",
|
||||
string.Empty,
|
||||
"```rust",
|
||||
"fn main() {}",
|
||||
"```");
|
||||
|
||||
var files = PlainFileExport.ExtractFiles(answer, ',');
|
||||
var partStart = answer.LastIndexOf("$$", StringComparison.Ordinal) + "$$".Length + Environment.NewLine.Length;
|
||||
var part = Markdig.Markdown.Parse(answer[partStart..], Markdown.CHAT_MARKDOWN_PIPELINE);
|
||||
var blockOfThePart = part.Descendants<FencedCodeBlock>().Single();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(files, Has.Count.EqualTo(2));
|
||||
Assert.That(partStart + blockOfThePart.Span.Start, Is.EqualTo(files[1].Position));
|
||||
Assert.That(files[1].ToName(), Is.EqualTo("Rust (.rs)"));
|
||||
});
|
||||
}
|
||||
|
||||
private static string Lines(params string[] lines) => string.Join(Environment.NewLine, lines);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tests.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Checks which file a code block becomes, judged by the language the model names behind its
|
||||
/// opening fence.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public sealed class SourceCodeLanguagesTests
|
||||
{
|
||||
[TestCase("python", "Python (.py)")]
|
||||
[TestCase("py", "Python (.py)")]
|
||||
[TestCase("rust", "Rust (.rs)")]
|
||||
[TestCase("rs", "Rust (.rs)")]
|
||||
[TestCase("javascript", "JavaScript (.js)")]
|
||||
[TestCase("js", "JavaScript (.js)")]
|
||||
[TestCase("bash", "Shell (.sh)")]
|
||||
[TestCase("sh", "Shell (.sh)")]
|
||||
[TestCase("c#", "C# (.cs)")]
|
||||
[TestCase("C++", "C++ (.cpp)")]
|
||||
[TestCase("Python", "Python (.py)", Description = "Models do not agree on the case.")]
|
||||
[TestCase(" yaml ", "YAML (.yaml)", Description = "Space around the name is no part of it.")]
|
||||
public void AKnownLanguageNamesItsFile(string language, string expectedName)
|
||||
{
|
||||
Assert.That(SourceCodeLanguages.FromCodeFenceLanguage(language).ToName(), Is.EqualTo(expectedName));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AnUnknownLanguageBecomesATextFileUnderItsOwnName()
|
||||
{
|
||||
var language = SourceCodeLanguages.FromCodeFenceLanguage("foo");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(language.Name, Is.EqualTo("foo"), "The user still has to recognize which block it is.");
|
||||
Assert.That(language.FileExtension, Is.EqualTo(".txt"));
|
||||
Assert.That(language.ToName(), Is.EqualTo("foo (.txt)"));
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(null, TestName = "A fence Markdig read no language for")]
|
||||
[TestCase("", TestName = "A fence without a language")]
|
||||
[TestCase(" ", TestName = "A language made of nothing")]
|
||||
[TestCase("text", TestName = "A fence marked as text")]
|
||||
[TestCase("PlainText", TestName = "A fence marked as plain text")]
|
||||
public void PlainTextBecomesATextFile(string? language)
|
||||
{
|
||||
Assert.That(SourceCodeLanguages.FromCodeFenceLanguage(language).ToName(), Is.EqualTo("Text (.txt)"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AFileOfALanguageIsNamedAfterWhatItIsAbout()
|
||||
{
|
||||
var python = SourceCodeLanguages.FromCodeFenceLanguage("python");
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(python.ToSuggestedFileName("Sort: a list?"), Is.EqualTo("Sort a list.py"));
|
||||
Assert.That(python.ToSuggestedFileName(), Is.EqualTo("export.py"));
|
||||
Assert.That(python.ToFileExtensionOrName(), Is.EqualTo(".py"));
|
||||
Assert.That(python.ToFileTypeFilter().FlattenExtensions(), Is.EqualTo(new[] { "py" }), "The dialog offers the one extension we write, without its dot.");
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase("dockerfile", "Dockerfile")]
|
||||
[TestCase("Makefile", "Makefile")]
|
||||
[TestCase("cmake", "CMakeLists.txt")]
|
||||
public void AToolWhichLooksForAFixedFileNameGetsIt(string language, string expectedFileName)
|
||||
{
|
||||
var fixedName = SourceCodeLanguages.FromCodeFenceLanguage(language);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(fixedName.ToSuggestedFileName("Backend"), Is.EqualTo(expectedFileName), "The tool finds no file under another name, whatever the heading above the block says.");
|
||||
Assert.That(fixedName.ToName(), Is.EqualTo(expectedFileName));
|
||||
Assert.That(fixedName.ToFileExtensionOrName(), Is.EqualTo(expectedFileName));
|
||||
Assert.That(FileTypes.ToFileDialogFilter(fixedName.ToFileTypeFilter()), Is.Null, "A file name is no extension, so the dialog shows every file.");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnlyPowerShellNeedsAByteOrderMark()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(SourceCodeLanguages.FromCodeFenceLanguage("powershell").ToFileEncoding().GetPreamble(), Is.Not.Empty, "Windows PowerShell 5.1 reads a script without one in the ANSI code page.");
|
||||
Assert.That(SourceCodeLanguages.FromCodeFenceLanguage("python").ToFileEncoding().GetPreamble(), Is.Empty, "An interpreter has no use for it, and a shebang line breaks behind one.");
|
||||
Assert.That(SourceCodeLanguages.Languages.Where(language => language.NeedsByteOrderMark).Select(language => language.Name), Is.EqualTo(new[] { "PowerShell" }));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OnlyABatchFileNeedsWindowsLineEndings()
|
||||
{
|
||||
Assert.That(SourceCodeLanguages.Languages.Where(language => language.NeedsWindowsLineEndings).Select(language => language.Name), Is.EqualTo(new[] { "Batch" }), "The command prompt misses jump labels in a file whose lines end with LF alone.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EveryLanguageHasAFile()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var language in SourceCodeLanguages.Languages)
|
||||
{
|
||||
Assert.That(language.Name, Is.Not.Empty);
|
||||
Assert.That(language.FileExtension.Length is 0 ^ language.FileName.Length is 0, Is.True, $"{language.Name} needs either an extension or a fixed file name, not both.");
|
||||
if (language.FileExtension.Length > 0)
|
||||
Assert.That(language.FileExtension, Does.StartWith(".").And.Length.GreaterThan(1), $"{language.Name} has a broken extension.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoAliasIsNamedTwice()
|
||||
{
|
||||
Assert.That(SourceCodeLanguages.Aliases, Is.Unique.Using((IEqualityComparer<string>)StringComparer.OrdinalIgnoreCase), "Plain text is checked first, so a language with the same alias would never be found.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoAliasNamesAFormatOfItsOwn()
|
||||
{
|
||||
// FileExportFormatExtensions.TryFromCodeFenceLanguage is asked first, so such an entry would never be found:
|
||||
Assert.That(SourceCodeLanguages.Aliases.Where(alias => FileExportFormatExtensions.TryFromCodeFenceLanguage(alias, out _)), Is.Empty);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user