public void TestPrepareForLine() { var path = Path.Combine(TestDataPath, "TaggedLines.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); Assert.Empty(result.Diagnostics); stringTable = result.StringTable; bool prepareForLinesWasCalled = false; dialogue.PrepareForLinesHandler = (lines) => { // When the Dialogue realises it's about to run the Start // node, it will tell us that it's about to run these two // line IDs Assert.Equal(2, lines.Count()); Assert.Contains("line:test1", lines); Assert.Contains("line:test2", lines); // Ensure that these asserts were actually called prepareForLinesWasCalled = true; }; dialogue.SetProgram(result.Program); dialogue.SetNode("Start"); Assert.True(prepareForLinesWasCalled); }
public void TestInvalidCharactersInNodeTitle() { var path = Path.Combine(TestDataPath, "InvalidNodeTitle.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); Assert.NotEmpty(result.Diagnostics); }
public void TestInvalidCharactersInNodeTitle() { var path = Path.Combine(TestDataPath, "InvalidNodeTitle.yarn"); Assert.Throws <Yarn.Compiler.ParseException>(() => { Compiler.Compile(CompilationJob.CreateFromFiles(path)); }); }
public void TestDumpingCode() { var path = Path.Combine(TestDataPath, "Example.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); var byteCode = dialogue.GetByteCode(); Assert.NotNull(byteCode); }
public void TestMissingNode() { var path = Path.Combine(TestDataPath, "TestCases", "Smileys.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); errorsCauseFailures = false; Assert.Throws <DialogueException>(() => dialogue.SetNode("THIS NODE DOES NOT EXIST")); }
public void TestAnalysis() { ICollection <Yarn.Analysis.Diagnosis> diagnoses; Yarn.Analysis.Context context; // this script has the following variables: // $foo is read from and written to // $bar is written to but never read // this means that there should be one diagnosis result context = new Yarn.Analysis.Context(typeof(Yarn.Analysis.UnusedVariableChecker)); var path = Path.Combine(TestDataPath, "AnalysisTest.yarn"); CompilationJob compilationJob = CompilationJob.CreateFromFiles(path); compilationJob.Library = dialogue.Library; var result = Compiler.Compile(compilationJob); Assert.Empty(result.Diagnostics); stringTable = result.StringTable; dialogue.SetProgram(result.Program); dialogue.Analyse(context); diagnoses = new List <Yarn.Analysis.Diagnosis>(context.FinishAnalysis()); Assert.Equal(1, diagnoses.Count); Assert.Contains("Variable $bar is assigned, but never read from", diagnoses.First().message); dialogue.UnloadAll(); context = new Yarn.Analysis.Context(typeof(Yarn.Analysis.UnusedVariableChecker)); result = Compiler.Compile(CompilationJob.CreateFromFiles(new[] { Path.Combine(SpaceDemoScriptsPath, "Ship.yarn"), Path.Combine(SpaceDemoScriptsPath, "Sally.yarn"), }, dialogue.Library)); Assert.Empty(result.Diagnostics); dialogue.SetProgram(result.Program); dialogue.Analyse(context); diagnoses = new List <Yarn.Analysis.Diagnosis>(context.FinishAnalysis()); // This script should contain no unused variables Assert.Empty(diagnoses); }
public void TestExampleScript() { errorsCauseFailures = false; var path = Path.Combine(TestDataPath, "Example.yarn"); var testPath = Path.ChangeExtension(path, ".testplan"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); stringTable = result.StringTable; this.LoadTestPlan(testPath); RunStandardTestcase(); }
public void TestGettingTags() { var path = Path.Combine(TestDataPath, "Example.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); var source = dialogue.GetTagsForNode("LearnMore"); Assert.NotNull(source); Assert.NotEmpty(source); Assert.Equal("rawText", source.First()); }
public void TestEndOfNotesWithOptionsNotAdded() { var path = Path.Combine(TestDataPath, "SkippedOptions.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); stringTable = result.StringTable; dialogue.OptionsHandler = delegate(OptionSet optionSets) { Assert.False(true, "Options should not be shown to the user in this test."); }; dialogue.SetNode(); dialogue.Continue(); }
public void TestGettingRawSource() { var path = Path.Combine(TestDataPath, "Example.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); stringTable = result.StringTable; var sourceID = dialogue.GetStringIDForNode("LearnMore"); var source = stringTable[sourceID].text; Assert.NotNull(source); Assert.Equal("A: HAHAHA\n", source); }
public void TestLoadingNodes() { var path = Path.Combine(TestDataPath, "Projects", "Basic", "Test.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); dialogue.SetProgram(result.Program); stringTable = result.StringTable; // high-level test: load the file, verify it has the nodes we want, // and run one Assert.Equal(3, dialogue.NodeNames.Count()); Assert.True(dialogue.NodeExists("TestNode")); Assert.True(dialogue.NodeExists("AnotherTestNode")); Assert.True(dialogue.NodeExists("ThirdNode")); }
public void TestNodeHeaders() { var path = Path.Combine(TestDataPath, "Headers.yarn"); var result = Compiler.Compile(CompilationJob.CreateFromFiles(path)); Assert.Equal(4, result.Program.Nodes.Count); foreach (var tag in new[] { "one", "two", "three" }) { Assert.Contains(tag, result.Program.Nodes["Tags"].Tags); } // Assert.Contains("version:2", result.FileTags); Assert.Contains(path, result.FileTags.Keys); Assert.Equal(1, result.FileTags.Count); Assert.Equal(1, result.FileTags[path].Count()); Assert.Contains("file_header", result.FileTags[path]); }
public void TestNodeExists() { var path = Path.Combine(SpaceDemoScriptsPath, "Sally.yarn"); CompilationJob compilationJob = CompilationJob.CreateFromFiles(path); compilationJob.Library = dialogue.Library; var result = Compiler.Compile(compilationJob); dialogue.SetProgram(result.Program); Assert.True(dialogue.NodeExists("Sally")); // Test clearing everything dialogue.UnloadAll(); Assert.False(dialogue.NodeExists("Sally")); }
/// <summary> /// Generates a collection of <see cref="StringTableEntry"/> /// objects, one for each line in this Yarn Project's scripts. /// </summary> /// <returns>An IEnumerable containing a <see /// cref="StringTableEntry"/> for each of the lines in the Yarn /// Project, or <see langword="null"/> if the Yarn Project contains /// errors.</returns> internal IEnumerable <StringTableEntry> GenerateStringsTable() { var pathsToImporters = sourceScripts.Where(s => s != null).Select(s => AssetDatabase.GetAssetPath(s)); if (pathsToImporters.Count() == 0) { // We have no scripts to work with - return an empty // collection - there's no error, but there's no content // either return(new List <StringTableEntry>()); } // We now now compile! var job = CompilationJob.CreateFromFiles(pathsToImporters); job.CompilationType = CompilationJob.Type.StringsOnly; CompilationResult compilationResult; try { compilationResult = Compiler.Compiler.Compile(job); } catch (ParseException) { Debug.LogError($"Can't generate a strings table from a Yarn Project that contains compile errors", null); return(null); } IEnumerable <StringTableEntry> stringTableEntries = compilationResult.StringTable.Select(x => new StringTableEntry { ID = x.Key, Language = defaultLanguage, Text = x.Value.text, File = x.Value.fileName, Node = x.Value.nodeName, LineNumber = x.Value.lineNumber.ToString(), Lock = YarnImporter.GetHashString(x.Value.text, 8), }); return(stringTableEntries); }
public void TestMergingNodes() { var sallyPath = Path.Combine(SpaceDemoScriptsPath, "Sally.yarn"); var shipPath = Path.Combine(SpaceDemoScriptsPath, "Ship.yarn"); CompilationJob compilationJobSally = CompilationJob.CreateFromFiles(sallyPath); CompilationJob compilationJobSallyAndShip = CompilationJob.CreateFromFiles(sallyPath, shipPath); compilationJobSally.Library = dialogue.Library; compilationJobSallyAndShip.Library = dialogue.Library; var resultSally = Compiler.Compile(compilationJobSally); var resultSallyAndShip = Compiler.Compile(compilationJobSallyAndShip); // Loading code with the same contents should throw Assert.Throws <InvalidOperationException>(delegate() { var combinedNotWorking = Program.Combine(resultSally.Program, resultSallyAndShip.Program); }); }
public void TestGettingCurrentNodeName() { string path = Path.Combine(SpaceDemoScriptsPath, "Sally.yarn"); CompilationJob compilationJob = CompilationJob.CreateFromFiles(path); compilationJob.Library = dialogue.Library; var result = Compiler.Compile(compilationJob); dialogue.SetProgram(result.Program); // dialogue should not be running yet Assert.Null(dialogue.CurrentNode); dialogue.SetNode("Sally"); Assert.Equal("Sally", dialogue.CurrentNode); dialogue.Stop(); // Current node should now be null Assert.Null(dialogue.CurrentNode); }
public void TestUpgradingV1toV2(string directory) { Console.ForegroundColor = ConsoleColor.Blue; Console.WriteLine($"INFO: Loading file {directory}"); storage.Clear(); directory = Path.Combine(TestBase.TestDataPath, directory); var allInputYarnFiles = Directory.EnumerateFiles(directory) .Where(path => path.EndsWith(".yarn")) .Where(path => path.Contains(".upgraded.") == false); var expectedOutputFiles = Directory.EnumerateFiles(directory) .Where(path => path.Contains(".upgraded.")); var testPlanPath = Directory.EnumerateFiles(directory) .Where(path => path.EndsWith(".testplan")) .FirstOrDefault(); var upgradeJob = new UpgradeJob( UpgradeType.Version1to2, allInputYarnFiles.Select(path => new CompilationJob.File { FileName = path, Source = File.ReadAllText(path) })); var upgradeResult = LanguageUpgrader.Upgrade(upgradeJob); // The upgrade result should produce as many files as there are // expected output files Assert.Equal(expectedOutputFiles.Count(), upgradeResult.Files.Count()); // For each file produced by the upgrade job, its content // should match that of the corresponding expected output foreach (var outputFile in upgradeResult.Files) { string extension = Path.GetExtension(outputFile.Path); var expectedOutputFilePath = Path.ChangeExtension(outputFile.Path, ".upgraded" + extension); if (expectedOutputFiles.Contains(expectedOutputFilePath) == false) { // This test case doesn't expect this output (perhaps // it's a test case that isn't expected to succeed.) Ignore it. continue; } Assert.True(File.Exists(expectedOutputFilePath), $"Expected file {expectedOutputFilePath} to exist"); var expectedOutputFileContents = File.ReadAllText(expectedOutputFilePath); Assert.Equal(expectedOutputFileContents, outputFile.UpgradedSource); } // If the test case doesn't contain a test plan file, it's not // expected to compile successfully, so don't do it. Instead, // we'll rely on the fact that the upgraded contents are what // we expected. if (testPlanPath == null) { // Don't compile; just succeed here. return; } // While we're here, correctness-check the upgraded source. (To // be strictly correct, we're using the files on disk, not the // generated source, but we just demonstrated that they're // identical, so that's fine! Saves us having to write them to // a temporary location.) var result = Compiler.Compile(CompilationJob.CreateFromFiles(expectedOutputFiles)); Assert.Empty(result.Diagnostics); stringTable = result.StringTable; // Execute the program and verify thats output matches the test // plan dialogue.SetProgram(result.Program); // Load the test plan LoadTestPlan(testPlanPath); // If this file contains a Start node, run the test case // (otherwise, we're just testing its parsability, which we did // in the last line) if (dialogue.NodeExists("Start")) { RunStandardTestcase(); } }
public override void OnImportAsset(AssetImportContext ctx) { #if YARNSPINNER_DEBUG UnityEngine.Profiling.Profiler.enabled = true; #endif var project = ScriptableObject.CreateInstance <YarnProject>(); project.name = Path.GetFileNameWithoutExtension(ctx.assetPath); // Start by creating the asset - no matter what, we need to // produce an asset, even if it doesn't contain valid Yarn // bytecode, so that other assets don't lose their references. ctx.AddObjectToAsset("Project", project); ctx.SetMainObject(project); foreach (var script in sourceScripts) { string path = AssetDatabase.GetAssetPath(script); if (string.IsNullOrEmpty(path)) { // This is, for some reason, not a valid script we can // use. Don't add a dependency on it. continue; } ctx.DependsOnSourceAsset(path); } // Parse declarations var localDeclarationsCompileJob = CompilationJob.CreateFromFiles(ctx.assetPath); localDeclarationsCompileJob.CompilationType = CompilationJob.Type.DeclarationsOnly; IEnumerable <Declaration> localDeclarations; compileError = null; try { var result = Compiler.Compiler.Compile(localDeclarationsCompileJob); localDeclarations = result.Declarations; } catch (ParseException e) { ctx.LogImportError($"Error in Yarn Project: {e.Message}"); compileError = $"Error in Yarn Project {ctx.assetPath}: {e.Message}"; return; } // Store these so that we can continue displaying them after // this import step, in case there are compile errors later. // We'll replace this with a more complete list later if // compilation succeeds. serializedDeclarations = localDeclarations .Where(decl => decl.DeclarationType == Declaration.Type.Variable) .Select(decl => new SerializedDeclaration(decl)).ToList(); // We're done processing this file - we've parsed it, and // pulled any information out of it that we need to. Now to // compile the scripts associated with this project. var scriptImporters = sourceScripts.Where(s => s != null).Select(s => AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(s)) as YarnImporter); // First step: check to see if there's any parse errors in the // files. var scriptsWithParseErrors = scriptImporters.Where(script => script.isSuccessfullyParsed == false); if (scriptsWithParseErrors.Count() != 0) { // Parse errors! We can't continue. string failingScriptNameList = string.Join("\n", scriptsWithParseErrors.Select(script => script.assetPath)); compileError = $"Parse errors exist in the following files:\n{failingScriptNameList}"; return; } // Get paths to the scripts we're importing, and also map them // to their corresponding importer var pathsToImporters = scriptImporters.ToDictionary(script => script.assetPath, script => script); if (pathsToImporters.Count == 0) { return; // nothing further to do here } // We now now compile! var job = CompilationJob.CreateFromFiles(pathsToImporters.Keys); job.VariableDeclarations = localDeclarations; CompilationResult compilationResult; try { compilationResult = Compiler.Compiler.Compile(job); } catch (TypeException e) { ctx.LogImportError($"Error compiling: {e.Message}"); compileError = e.Message; var importer = pathsToImporters[e.FileName]; importer.parseErrorMessage = e.Message; EditorUtility.SetDirty(importer); return; } catch (ParseException e) { ctx.LogImportError(e.Message); compileError = e.Message; var importer = pathsToImporters[e.FileName]; importer.parseErrorMessage = e.Message; EditorUtility.SetDirty(importer); return; } if (compilationResult.Program == null) { ctx.LogImportError("Internal error: Failed to compile: resulting program was null, but compiler did not throw a parse exception."); return; } // Store _all_ declarations - both the ones in this // .yarnproject file, and the ones inside the .yarn files serializedDeclarations = localDeclarations .Concat(compilationResult.Declarations) .Where(decl => decl.DeclarationType == Declaration.Type.Variable) .Select(decl => new SerializedDeclaration(decl)).ToList(); // Clear error messages from all scripts - they've all passed // compilation foreach (var importer in pathsToImporters.Values) { importer.parseErrorMessage = null; } // Will we need to create a default localization? This variable // will be set to false if any of the languages we've // configured in languagesToSourceAssets is the default // language. var shouldAddDefaultLocalization = true; foreach (var pair in languagesToSourceAssets) { // Don't create a localization if the language ID was not // provided if (string.IsNullOrEmpty(pair.languageID)) { Debug.LogWarning($"Not creating a localization for {project.name} because the language ID wasn't provided. Add the language ID to the localization in the Yarn Project's inspector."); continue; } IEnumerable <StringTableEntry> stringTable; // Where do we get our strings from? If it's the default // language, we'll pull it from the scripts. If it's from // any other source, we'll pull it from the CSVs. if (pair.languageID == defaultLanguage) { // We'll use the program-supplied string table. stringTable = GenerateStringsTable(); // We don't need to add a default localization. shouldAddDefaultLocalization = false; } else { try { if (pair.stringsFile == null) { // We can't create this localization because we // don't have any data for it. Debug.LogWarning($"Not creating a localization for {pair.languageID} in the Yarn Project {project.name} because a text asset containing the strings wasn't found. Add a .csv file containing the translated lines to the Yarn Project's inspector."); continue; } stringTable = StringTableEntry.ParseFromCSV(pair.stringsFile.text); } catch (System.ArgumentException e) { Debug.LogWarning($"Not creating a localization for {pair.languageID} in the Yarn Project {project.name} because an error was encountered during text parsing: {e}"); continue; } } var newLocalization = ScriptableObject.CreateInstance <Localization>(); newLocalization.LocaleCode = pair.languageID; newLocalization.AddLocalizedStrings(stringTable); project.localizations.Add(newLocalization); newLocalization.name = pair.languageID; if (pair.assetsFolder != null) { var assetsFolderPath = AssetDatabase.GetAssetPath(pair.assetsFolder); if (assetsFolderPath == null) { // This was somehow not a valid reference? Debug.LogWarning($"Can't find assets for localization {pair.languageID} in {project.name} because a path for the provided assets folder couldn't be found."); } else { var stringIDsToAssets = FindAssetsForLineIDs(stringTable.Select(s => s.ID), assetsFolderPath); #if YARNSPINNER_DEBUG var stopwatch = System.Diagnostics.Stopwatch.StartNew(); #endif newLocalization.AddLocalizedObjects(stringIDsToAssets.AsEnumerable()); #if YARNSPINNER_DEBUG stopwatch.Stop(); Debug.Log($"Imported {stringIDsToAssets.Count()} assets for {project.name} \"{pair.languageID}\" in {stopwatch.ElapsedMilliseconds}ms"); #endif } } ctx.AddObjectToAsset("localization-" + pair.languageID, newLocalization); if (pair.languageID == defaultLanguage) { // If this is our default language, set it as such project.baseLocalization = newLocalization; } else { // This localization depends upon a source asset. Make // this asset get re-imported if this source asset was // modified ctx.DependsOnSourceAsset(AssetDatabase.GetAssetPath(pair.stringsFile)); } } if (shouldAddDefaultLocalization) { // We didn't add a localization for the default language. // Create one for it now. var developmentLocalization = ScriptableObject.CreateInstance <Localization>(); developmentLocalization.LocaleCode = defaultLanguage; var stringTableEntries = compilationResult.StringTable.Select(x => new StringTableEntry { ID = x.Key, Language = defaultLanguage, Text = x.Value.text, File = x.Value.fileName, Node = x.Value.nodeName, LineNumber = x.Value.lineNumber.ToString(), Lock = YarnImporter.GetHashString(x.Value.text, 8), }); developmentLocalization.AddLocalizedStrings(stringTableEntries); project.baseLocalization = developmentLocalization; project.localizations.Add(project.baseLocalization); developmentLocalization.name = $"Default ({defaultLanguage})"; ctx.AddObjectToAsset("default-language", developmentLocalization); } // Store the compiled program byte[] compiledBytes = null; using (var memoryStream = new MemoryStream()) using (var outputStream = new Google.Protobuf.CodedOutputStream(memoryStream)) { // Serialize the compiled program to memory compilationResult.Program.WriteTo(outputStream); outputStream.Flush(); compiledBytes = memoryStream.ToArray(); } project.compiledYarnProgram = compiledBytes; #if YARNSPINNER_DEBUG UnityEngine.Profiling.Profiler.enabled = false; #endif }