mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 02:07:21 +00:00
183 lines
8.9 KiB
C#
183 lines
8.9 KiB
C#
using AIStudio.Tools.CodeHighlighting;
|
|||
|
|
|
||
|
|
namespace AIStudio.Tests.Tools.CodeHighlighting;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Checks how the tokenizer reads code, with a small language made up for the tests. The tests of
|
||
|
|
/// each real language follow in their own files.
|
||
|
|
/// </summary>
|
||
|
|
[TestFixture]
|
||
|
|
public sealed class CodeTokenizerTests
|
||
|
|
{
|
||
|
|
private static readonly CodeLanguageRules TEST_LANGUAGE = new()
|
||
|
|
{
|
||
|
|
Keywords = CodeLanguageRules.Words("def", "class", "if", "return"),
|
||
|
|
Types = CodeLanguageRules.Words("int"),
|
||
|
|
Literals = CodeLanguageRules.Words("true", "null"),
|
||
|
|
FunctionDeclarations = CodeLanguageRules.Words("def"),
|
||
|
|
TypeDeclarations = CodeLanguageRules.Words("class"),
|
||
|
|
LineComments = ["//"],
|
||
|
|
BlockComments = [new("/*", "*/")],
|
||
|
|
Strings =
|
||
|
|
[
|
||
|
|
new("\"\"\"", "\"\"\"", IsMultiline: true),
|
||
|
|
new("\"", "\""),
|
||
|
|
new("'", "'", Escape: null, DoubledCloseIsLiteral: true),
|
||
|
|
],
|
||
|
|
StringPrefixes = CodeLanguageRules.Words("r"),
|
||
|
|
VariablePrefix = '$',
|
||
|
|
AnnotationPrefix = '@',
|
||
|
|
};
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// The same language with all habits of the real languages switched on, for the test which feeds
|
||
|
|
/// it random characters.
|
||
|
|
/// </summary>
|
||
|
|
private static readonly CodeLanguageRules TEST_LANGUAGE_WITH_ALL_HABITS = new()
|
||
|
|
{
|
||
|
|
Keywords = CodeLanguageRules.Words("def", "class", "if", "return"),
|
||
|
|
Literals = CodeLanguageRules.Words("true"),
|
||
|
|
FunctionDeclarations = CodeLanguageRules.Words("def"),
|
||
|
|
TypeDeclarations = CodeLanguageRules.Words("class"),
|
||
|
|
LineComments = ["#", "--"],
|
||
|
|
LineCommentsNeedSeparator = true,
|
||
|
|
BlockComments = [new("/*", "*/"), new("<#", "#>")],
|
||
|
|
Strings = [new("\"\"\"", "\"\"\"", IsMultiline: true), new("\"", "\"", Escape: '`'), new("'", "'", Escape: null, DoubledCloseIsLiteral: true)],
|
||
|
|
QuotedNames = [new("`", "`", Escape: null)],
|
||
|
|
StringPrefixes = CodeLanguageRules.Words("r", "L"),
|
||
|
|
HasLongBrackets = true,
|
||
|
|
VariablePrefix = '$',
|
||
|
|
VariableNameCharacters = ":",
|
||
|
|
AnnotationPrefix = '@',
|
||
|
|
HasPreprocessor = true,
|
||
|
|
WordStartCharacters = "-$",
|
||
|
|
WordCharacters = "-",
|
||
|
|
HighlightPascalCaseTypes = true,
|
||
|
|
HighlightCommandNames = true,
|
||
|
|
HyphenOperators = CodeLanguageRules.WordsInAnyCase("eq", "ne"),
|
||
|
|
HasBracketTypes = true,
|
||
|
|
IsStyleSheet = true,
|
||
|
|
};
|
||
|
|
|
||
|
|
[TestCase("def greet(name):", "{keyword:def} {function:greet}(name):")]
|
||
|
|
[TestCase("class Point:", "{keyword:class} {type:Point}:")]
|
||
|
|
[TestCase("def (x)", "{keyword:def} (x)", Description = "What a keyword expects ends at anything but a name.")]
|
||
|
|
[TestCase("if x return true", "{keyword:if} x {keyword:return} {literal:true}")]
|
||
|
|
[TestCase("int count = 42", "{type:int} count = {number:42}")]
|
||
|
|
[TestCase("print(x)", "{function:print}(x)")]
|
||
|
|
[TestCase("print (x)", "print (x)", Description = "Only a parenthesis right after a name makes it a call.")]
|
||
|
|
[TestCase("Define x", "Define x", Description = "A keyword matches only as it is written.")]
|
||
|
|
[TestCase("x1 = y2", "x1 = y2", Description = "Digits within a name make no number.")]
|
||
|
|
public void WordsGetTheColorOfWhatTheyAre(string code, string expected)
|
||
|
|
{
|
||
|
|
Assert.That(CodeMarkup.Of(code, TEST_LANGUAGE), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("a // note\nb", "a {comment:// note}\nb")]
|
||
|
|
[TestCase("a // note\r\nb", "a {comment:// note}\r\nb", Description = "The line break is no part of the comment.")]
|
||
|
|
[TestCase("a /* one\ntwo */ b", "a {comment:/* one\ntwo */} b")]
|
||
|
|
[TestCase("a /* never closed\nb", "a {comment:/* never closed\nb}", Description = "A model which is still writing has not closed it yet.")]
|
||
|
|
[TestCase("\"a // b\" // c", "{string:\"a // b\"} {comment:// c}", Description = "Within a string, nothing starts a comment.")]
|
||
|
|
public void CommentsRunToTheirEnd(string code, string expected)
|
||
|
|
{
|
||
|
|
Assert.That(CodeMarkup.Of(code, TEST_LANGUAGE), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("\"a\\\"b\" c", "{string:\"a\\\"b\"} c", Description = "An escaped quotation mark does not close the string.")]
|
||
|
|
[TestCase("'it''s' x", "{string:'it''s'} x", Description = "Two apostrophes stand for one.")]
|
||
|
|
[TestCase("'a\\' x", "{string:'a\\'} x", Description = "A string without escapes ends at its first closing mark.")]
|
||
|
|
[TestCase("\"open\nnext", "{string:\"open}\nnext", Description = "A string of one line ends with its line, closed or not.")]
|
||
|
|
[TestCase("\"\"\"one\ntwo\"\"\" x", "{string:\"\"\"one\ntwo\"\"\"} x")]
|
||
|
|
[TestCase("\"\"\"never closed\nx", "{string:\"\"\"never closed\nx}")]
|
||
|
|
[TestCase("r\"raw\" r = 1", "{string:r\"raw\"} r = {number:1}", Description = "A prefix belongs to the string only right before it.")]
|
||
|
|
[TestCase("\"if\"", "{string:\"if\"}", Description = "A keyword within a string stays a part of it.")]
|
||
|
|
public void StringsRunToTheirEnd(string code, string expected)
|
||
|
|
{
|
||
|
|
Assert.That(CodeMarkup.Of(code, TEST_LANGUAGE), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("42", "{number:42}")]
|
||
|
|
[TestCase("0x1F", "{number:0x1F}")]
|
||
|
|
[TestCase("0b1010", "{number:0b1010}")]
|
||
|
|
[TestCase("1.5e-3", "{number:1.5e-3}")]
|
||
|
|
[TestCase("1_000", "{number:1_000}")]
|
||
|
|
[TestCase("10UL 1.5f", "{number:10UL} {number:1.5f}")]
|
||
|
|
[TestCase(".5", "{number:.5}")]
|
||
|
|
[TestCase("1..5", "{number:1}..{number:5}", Description = "A range is no decimal point.")]
|
||
|
|
[TestCase("a.b", "a.b")]
|
||
|
|
public void NumbersIncludeTheirNotation(string code, string expected)
|
||
|
|
{
|
||
|
|
Assert.That(CodeMarkup.Of(code, TEST_LANGUAGE), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("$name", "{variable:$name}")]
|
||
|
|
[TestCase("${a b}", "{variable:${a b}}")]
|
||
|
|
[TestCase("$? $1", "{variable:$?} {variable:$1}")]
|
||
|
|
[TestCase("$(cmd)", "$(cmd)", Description = "A command substitution is no variable.")]
|
||
|
|
[TestCase("@Override", "{meta:@Override}")]
|
||
|
|
[TestCase("@app.route('/')", "{meta:@app.route}({string:'/'})")]
|
||
|
|
[TestCase("a @ b", "a @ b", Description = "An operator is no annotation.")]
|
||
|
|
public void PrefixesMarkVariablesAndAnnotations(string code, string expected)
|
||
|
|
{
|
||
|
|
Assert.That(CodeMarkup.Of(code, TEST_LANGUAGE), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("# note", "{comment:# note}")]
|
||
|
|
[TestCase("echo $#", "echo {variable:$#}")]
|
||
|
|
[TestCase("a#b", "a#b")]
|
||
|
|
[TestCase("a; # note", "a; {comment:# note}")]
|
||
|
|
public void ACommentWhichNeedsASeparatorStartsOnlyAtTheStartOfAWord(string code, string expected)
|
||
|
|
{
|
||
|
|
var shellLike = new CodeLanguageRules { LineComments = ["#"], LineCommentsNeedSeparator = true, VariablePrefix = '$' };
|
||
|
|
Assert.That(CodeMarkup.Of(code, shellLike), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("#include <a.h>\nint x;", "{meta:#include <a.h>}\n{type:int} x;")]
|
||
|
|
[TestCase(" #define X 1", " {meta:#define X 1}")]
|
||
|
|
[TestCase("x # y", "x # y", Description = "Only a # at the start of a line begins a directive.")]
|
||
|
|
public void ADirectiveRunsToTheEndOfItsLine(string code, string expected)
|
||
|
|
{
|
||
|
|
var cLike = new CodeLanguageRules { Types = CodeLanguageRules.Words("int"), HasPreprocessor = true };
|
||
|
|
Assert.That(CodeMarkup.Of(code, cLike), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[TestCase("HashMap map", "{type:HashMap} map")]
|
||
|
|
[TestCase("URL x", "URL x", Description = "A constant in capitals is no type.")]
|
||
|
|
public void PascalCaseNamesTypesWhereTheLanguageSaysSo(string code, string expected)
|
||
|
|
{
|
||
|
|
var javaLike = new CodeLanguageRules { HighlightPascalCaseTypes = true };
|
||
|
|
Assert.That(CodeMarkup.Of(code, javaLike), Is.EqualTo(expected));
|
||
|
|
}
|
||
|
|
|
||
|
|
[Test]
|
||
|
|
public void EmptyCodeHasNoParts()
|
||
|
|
{
|
||
|
|
Assert.That(CodeTokenizer.Tokenize(string.Empty, TEST_LANGUAGE), Is.Empty);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// A model writes anything, and a streamed answer stops anywhere. Whatever the code, the parts stay
|
||
|
|
/// in order, inside the code, and never overlap.
|
||
|
|
/// </summary>
|
||
|
|
[Test]
|
||
|
|
public void AnyCodeGivesPartsInOrder([Values(1, 2, 3, 4, 5, 6, 7, 8)] int seed)
|
||
|
|
{
|
||
|
|
const string CHARACTERS = "\"'`\\/*#$@{}()[]=-.:;!%<>abcdefLrXYZ019_ \n\r\t";
|
||
|
|
var random = new Random(seed);
|
||
|
|
foreach (var rules in new[] { TEST_LANGUAGE, TEST_LANGUAGE_WITH_ALL_HABITS })
|
||
|
|
{
|
||
|
|
for (var sample = 0; sample < 200; sample++)
|
||
|
|
{
|
||
|
|
var code = new string(Enumerable.Range(0, random.Next(1, 120)).Select(_ => CHARACTERS[random.Next(CHARACTERS.Length)]).ToArray());
|
||
|
|
var end = 0;
|
||
|
|
foreach (var token in CodeTokenizer.Tokenize(code, rules))
|
||
|
|
{
|
||
|
|
Assert.That(token.Start, Is.GreaterThanOrEqualTo(end), $"A part overlaps the one before it in: {code}");
|
||
|
|
Assert.That(token.Length, Is.GreaterThan(0), $"A part is empty in: {code}");
|
||
|
|
end = token.Start + token.Length;
|
||
|
|
Assert.That(end, Is.LessThanOrEqualTo(code.Length), $"A part runs past the end of: {code}");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|