using System.Buffers; using System.Text.Json; using PkmnLib.Dynamic.Libraries; using PkmnLib.Dynamic.Plugins; using PkmnLib.Dynamic.ScriptHandling; using PkmnLib.Dynamic.ScriptHandling.Registry; using PkmnLib.Static.Moves; namespace PkmnLib.Plugin.Gen7.Tests.DataTests; public class MoveDataTests { public record TestCaseData(IDynamicLibrary Library, IMoveData Move) { /// public override string ToString() => Move.Name + " has valid scripts"; } public static IEnumerable> AllMovesHaveValidScriptsData() { var library = LibraryHelpers.LoadLibrary(); var moveLibrary = library.StaticLibrary.Moves; foreach (var move in moveLibrary) { if (move.SecondaryEffect == null) continue; yield return () => new TestCaseData(library, move); } } [Test, MethodDataSource(nameof(AllMovesHaveValidScriptsData))] public async Task AllMoveEffectsHaveValidScripts(TestCaseData test) { if (test.Move.SecondaryEffect == null) return; var scriptName = test.Move.SecondaryEffect.Name; try { await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Move, scriptName, test.Move.SecondaryEffect.Parameters, out _)).IsTrue(); } catch (Exception e) { // Helper method to find the line number of the effect in the JSON file var file = Path.GetFullPath("../../../../Plugins/PkmnLib.Plugin.Gen7/Data/Moves.jsonc"); var json = await File.ReadAllLinesAsync(file); var moveLineNumber = json.Select((line, index) => new { line, index }) .FirstOrDefault(x => x.line.Contains($"\"name\": \"{test.Move.Name}\""))?.index + 1; var effectLineNumber = moveLineNumber + json.Skip(moveLineNumber ?? 0) .Select((line, index) => new { line, index }).FirstOrDefault(x => x.line.Contains("effect")) ?.index + 1 ?? 0; await TestContext.Current!.OutputWriter.WriteLineAsync("File: " + $"file://{file}:{effectLineNumber}"); throw new AggregateException($"Failed to resolve script for move {test.Move.Name} with effect {scriptName}", e); } } public record SetStatusTestCaseData(IDynamicLibrary Library, IMoveData Move) { /// public override string ToString() => Move.Name + " has valid status: " + Move.SecondaryEffect?.Parameters.GetValueOrDefault("status"); } public static IEnumerable> SetStatusMovesHaveValidStatusData() { var library = LibraryHelpers.LoadLibrary(); var moveLibrary = library.StaticLibrary.Moves; foreach (var move in moveLibrary) { if (move.SecondaryEffect?.Name != "set_status") continue; yield return () => new SetStatusTestCaseData(library, move); } } [Test, MethodDataSource(nameof(SetStatusMovesHaveValidStatusData))] public async Task SetStatusMovesHaveValidStatus(SetStatusTestCaseData test) { if (test.Move.SecondaryEffect == null) return; var status = test.Move.SecondaryEffect.Parameters["status"]?.ToString(); if (status == null) throw new Exception("Missing required parameter 'status'"); await Assert.That(test.Library.ScriptResolver.TryResolve(ScriptCategory.Status, status, null, out _)).IsTrue(); } public record HasEitherEffectOrComment(string MoveName, bool HasEffect, bool HasComment) { /// public override string ToString() => MoveName + " has effect: " + HasEffect + ", comment: " + HasComment; } public static IEnumerable> EveryMoveHasEitherEffectOrCommentData() { IResourceProvider plugin = new Gen7Plugin(); using var movesJson = plugin.GetResource(ResourceFileType.Moves)!.Open(); var json = new BinaryReader(movesJson); var moves = json.ReadBytes((int)movesJson.Length); var reader = new Utf8JsonReader(new ReadOnlySequence(moves), new JsonReaderOptions { CommentHandling = JsonCommentHandling.Allow, AllowTrailingCommas = true, }); Console.WriteLine("Reading moves.json"); // The JSON is an object { "data": [] }. // Each move is an object in the "data" array. // Each object needs to have either an "effect" or a "comment" property. // Read the first object reader.Read(); // Read the properties, until we find the "data" property while (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "data") { reader.Read(); } // Read the "data" property reader.Read(); // Read the start of the array reader.Read(); var results = new List(); // Read each move object while (reader.TokenType != JsonTokenType.EndArray) { // Read the start of the object if (!reader.Read()) break; // Read each property var name = ""; var hasEffect = false; var hasComment = false; while (reader.TokenType != JsonTokenType.EndObject) { if (reader.TokenType == JsonTokenType.PropertyName) { var propertyName = reader.GetString(); if (propertyName == "name") { reader.Read(); name = reader.GetString()!; } else if (propertyName == "effect") { hasEffect = true; reader.Read(); // Read the effect object while (reader.TokenType != JsonTokenType.EndObject) { reader.Read(); if (reader.TokenType != JsonTokenType.StartObject) continue; while (reader.TokenType != JsonTokenType.EndObject) { reader.Read(); } reader.Read(); } } } else if (reader.TokenType == JsonTokenType.Comment) { // Read the comment var comment = reader.GetComment(); hasComment = comment.Trim() == "No secondary effect"; } reader.Read(); } results.Add(new HasEitherEffectOrComment(name, hasEffect, hasComment)); } return results.Where(x => !string.IsNullOrWhiteSpace(x.MoveName)) .Select>(x => () => x).ToList(); } /// /// To ensure that all moves have been properly implemented, we require that each move has either an "effect" property, /// or a comment with the exact text "No secondary effect". This ensures that we have not missed any moves. /// /// The implementation of this test is a bit tricky, as we need to read the JSON file and parse it ourselves, /// as System.Text.Json does not support reading comments in JSON files beyond the low-level API. /// /// [Test, MethodDataSource(nameof(EveryMoveHasEitherEffectOrCommentData))] public async Task AllMovesHaveEitherEffectOrComment(HasEitherEffectOrComment test) => // Check that each move has either an "effect" or a "comment" property await Assert.That(test.HasEffect || test.HasComment).IsTrue(); }