Credits: credit every third-party work whose own text states holder and licence; test discovers all licence-like files
Adds Liberation Sans (OFL), UniRx, KDTree, webgl-noise simplex noise and ambientCG to the list. CreditsTests now matches LICEN[CS]E, COPYING, NOTICE, README, SOURCES, OFL files under Assets and requires each to be credited or listed as reviewed-not-credited with a reason; credit notices are checked against what their files state. The attribution heading is checked rather than skipped, the test states independent expectations, and the split Hurricane line is joined. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b367f80e6d
commit
16cb82d096
3 changed files with 188 additions and 25 deletions
|
|
@ -2,8 +2,7 @@ Noun Project
|
|||
|
||||
Tornado by Bart Laubsch
|
||||
Alien Abduction by Bart Laubsch
|
||||
Hurricane by icon 54 from
|
||||
Smashicons Space by Ben Davis
|
||||
Hurricane by icon 54 from Smashicons Space by Ben Davis
|
||||
Space 2 - Solid by Ben Davis
|
||||
Filter by Karthik Aathis
|
||||
Search by il Capitano
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ public static class Credits
|
|||
public const string TwemojiFile = "Assets/Fonts/Emoji/LICENSE.txt";
|
||||
public const string PlaydeadFile = "Assets/Plugins/Playdead/LICENSE.txt";
|
||||
public const string NounProjectFile = "Assets/Resources/Sprites/Attribution.txt";
|
||||
public const string LiberationFile = "Assets/TextMesh Pro/Fonts/LiberationSans - OFL.txt";
|
||||
public const string UniRxFile = "Assets/Plugins/UniRx/ReadMe.txt";
|
||||
public const string AmbientCgFile = "Assets/Materials/PBR/SOURCES.md";
|
||||
public const string KdTreeFile = "Assets/Scripts/ServerShared/NIH/KDTree/KDTree.cs";
|
||||
public const string SimplexNoiseFile = "Assets/Plugins/GPU Noise/SimplexNoise2D.cginc";
|
||||
// The first line of the attribution file; the icon credits are the lines after it.
|
||||
public const string NounProjectHeading = "Noun Project";
|
||||
// Where the player build loads the Noun Project credit lines from (Resources path, no extension).
|
||||
public const string NounProjectResource = "Sprites/Attribution";
|
||||
|
||||
|
|
@ -45,6 +52,44 @@ public static class Credits
|
|||
LicenceFile = PlaydeadFile
|
||||
},
|
||||
new Credit
|
||||
{
|
||||
Name = "Liberation Sans",
|
||||
Licence = "SIL Open Font License 1.1",
|
||||
Notice = "Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo, Tinos and Cousine. " +
|
||||
"Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation.",
|
||||
LicenceFile = LiberationFile
|
||||
},
|
||||
new Credit
|
||||
{
|
||||
Name = "UniRx",
|
||||
Licence = "MIT",
|
||||
Notice = "Created by Yoshifumi Kawai (neuecc)",
|
||||
LicenceFile = UniRxFile
|
||||
},
|
||||
new Credit
|
||||
{
|
||||
Name = "KDTree",
|
||||
Licence = "MIT",
|
||||
Notice = "Copyright (c) 2018 Vili Volčini / viliwonka",
|
||||
LicenceFile = KdTreeFile
|
||||
},
|
||||
new Credit
|
||||
{
|
||||
Name = "webgl-noise simplex noise",
|
||||
Licence = "MIT",
|
||||
Notice = "Copyright (C) 2011 Ashima Arts. Author: Ian McEwan, Ashima Arts " +
|
||||
"(https://github.com/ashima/webgl-noise). Translated to Unity shaders and modified by Keijiro Takahashi " +
|
||||
"(https://github.com/keijiro/NoiseShader).",
|
||||
LicenceFile = SimplexNoiseFile
|
||||
},
|
||||
new Credit
|
||||
{
|
||||
Name = "ambientCG PBR materials",
|
||||
Licence = "CC0 1.0 Universal",
|
||||
Notice = "Texture sets from ambientCG (https://ambientcg.com), public domain",
|
||||
LicenceFile = AmbientCgFile
|
||||
},
|
||||
new Credit
|
||||
{
|
||||
Name = "Noun Project icons",
|
||||
Licence = "Noun Project attribution licence",
|
||||
|
|
@ -56,9 +101,16 @@ public static class Credits
|
|||
return credits;
|
||||
}
|
||||
|
||||
// The icon credit lines of the attribution file: everything after its "Noun Project" heading.
|
||||
public static IEnumerable<string> NounProjectLines(string attribution) =>
|
||||
(attribution ?? "").Split('\n').Select(line => line.Trim()).Where(line => line.Length > 0).Skip(1);
|
||||
// The icon credit lines of the attribution file: everything after its "Noun Project" heading, one line per icon (the
|
||||
// file's "Other" sub-heading and its lines follow in the same list). A file that does not open with the heading is
|
||||
// refused rather than having its first line dropped.
|
||||
public static IEnumerable<string> NounProjectLines(string attribution)
|
||||
{
|
||||
var lines = (attribution ?? "").Split('\n').Select(line => line.Trim()).Where(line => line.Length > 0).ToArray();
|
||||
if (lines.Length < 2 || lines[0] != NounProjectHeading)
|
||||
throw new InvalidOperationException($"{NounProjectFile} must open with the heading '{NounProjectHeading}' followed by icon credits");
|
||||
return lines.Skip(1);
|
||||
}
|
||||
|
||||
// Throws on an entry a player-facing screen could not honestly show.
|
||||
public static void Validate(IReadOnlyList<Credit> credits)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
namespace Aetheria.Shared.Tests
|
||||
|
|
@ -19,41 +20,151 @@ namespace Aetheria.Shared.Tests
|
|||
|
||||
private static System.Collections.Generic.IReadOnlyList<Credit> Load() => Credits.All(Read(Credits.NounProjectFile));
|
||||
|
||||
[Fact]
|
||||
public void EveryLicenceFileUnderAssetsIsCredited()
|
||||
// A file under Assets that looks like a licence, attribution, notice, readme or sources list. Each one is either
|
||||
// credited (the credit names it as its LicenceFile) or reviewed below with the reason it is not.
|
||||
private static readonly Regex LicenceLike = new Regex(
|
||||
@"^(LICEN[CS]E|COPYING|NOTICE|README|SOURCES|_SOURCES|Attribution)|OFL", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly string[] SkippedDirectories = { "bin", "obj", "Library" };
|
||||
|
||||
// Discovered files whose own text names no holder and no licence, so there is nothing true to credit. A new
|
||||
// licence-like file is not here until someone has read it and written the reason.
|
||||
private static readonly System.Collections.Generic.Dictionary<string, string> ReviewedNotCredited = new System.Collections.Generic.Dictionary<string, string>
|
||||
{
|
||||
["Assets/Scripts/ServerShared/NIH/LinqFaster/readme.txt"] = "usage guide for LinqFaster; names no holder and no licence",
|
||||
["Assets/Plugins/LowPoly_AsteroidsPack/README.pdf"] = "how to customise the asteroid pack; names no holder and no licence",
|
||||
["Assets/Plugins/Celestial Body/Textures/_Sources.txt"] = "URLs of the source textures only; states no licence",
|
||||
};
|
||||
|
||||
// Bundled works with no licence-like file at all, found by reading Assets. Their licences are not stated in the
|
||||
// repo, so they are not credited; each path must still exist, so this list cannot go stale unnoticed.
|
||||
private static readonly string[] UnlicensedWorks =
|
||||
{
|
||||
"Assets/Fonts/Ubuntu",
|
||||
"Assets/Fonts/Retro",
|
||||
"Assets/Plugins/Ink",
|
||||
"Assets/Plugins/GPUTrail",
|
||||
"Assets/Plugins/com.unity.uiextensions",
|
||||
"Assets/Shaders/StableFluids",
|
||||
};
|
||||
|
||||
// What each credited file itself states, written here independently of the credit text: these strings must
|
||||
// appear in the file and in the credit's notice.
|
||||
private static readonly (string file, string[] stated)[] Holders =
|
||||
{
|
||||
(Credits.TwemojiFile, new[] { "Jason Sofonia & Justine De Caires", "Twitter", "CC BY 4.0" }),
|
||||
(Credits.PlaydeadFile, new[] { "Playdead" }),
|
||||
(Credits.LiberationFile, new[] { "Google Corporation", "Red Hat, Inc." }),
|
||||
(Credits.UniRxFile, new[] { "Yoshifumi Kawai" }),
|
||||
(Credits.KdTreeFile, new[] { "Vili Vol\u010dini / viliwonka" }),
|
||||
(Credits.SimplexNoiseFile, new[] { "Ashima Arts", "Ian McEwan", "Keijiro Takahashi" }),
|
||||
(Credits.AmbientCgFile, new[] { "ambientCG" }),
|
||||
};
|
||||
|
||||
private static string[] LicenceLikeFiles()
|
||||
{
|
||||
var root = Root();
|
||||
var found = Directory.EnumerateFiles(Path.Combine(root, "Assets"), "*", SearchOption.AllDirectories)
|
||||
return Directory.EnumerateFiles(Path.Combine(root, "Assets"), "*", SearchOption.AllDirectories)
|
||||
.Where(path => !path.EndsWith(".meta"))
|
||||
.Where(path => { var name = Path.GetFileName(path); return name.StartsWith("LICENSE", StringComparison.OrdinalIgnoreCase) || name.StartsWith("Attribution", StringComparison.OrdinalIgnoreCase); })
|
||||
.Select(path => Path.GetRelativePath(root, path).Replace('\\', '/'))
|
||||
.Where(path => !path.Split('/').Any(segment => SkippedDirectories.Contains(segment)))
|
||||
.Where(path => LicenceLike.IsMatch(Path.GetFileName(path)))
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
Assert.Contains(Credits.TwemojiFile, found);
|
||||
var credited = Load().Select(credit => credit.LicenceFile).ToHashSet();
|
||||
Assert.Empty(found.Where(path => !credited.Contains(path)));
|
||||
Assert.Empty(credited.Where(path => !File.Exists(Path.Combine(root, path))));
|
||||
}
|
||||
|
||||
// The ways a licence-like file can be accounted for; shared so the mutations in the report hit the same rule.
|
||||
private static string[] Unaccounted(string[] found, System.Collections.Generic.IEnumerable<string> credited) =>
|
||||
found.Where(path => !credited.Contains(path) && !ReviewedNotCredited.ContainsKey(path)).ToArray();
|
||||
|
||||
[Fact]
|
||||
public void TwemojiComesFirstWithTheHoldersItsLicenceNames()
|
||||
public void EveryLicenceLikeFileUnderAssetsIsCreditedOrReviewed()
|
||||
{
|
||||
var root = Root();
|
||||
var found = LicenceLikeFiles();
|
||||
Assert.Contains(Credits.TwemojiFile, found);
|
||||
Assert.Contains(Credits.LiberationFile, found);
|
||||
var credited = Load().Select(credit => credit.LicenceFile).ToArray();
|
||||
Assert.Equal(Array.Empty<string>(), Unaccounted(found, credited));
|
||||
Assert.Equal(Array.Empty<string>(), credited.Where(path => !File.Exists(Path.Combine(root, path))).ToArray());
|
||||
Assert.Equal(Array.Empty<string>(), ReviewedNotCredited.Keys.Where(path => !found.Contains(path)).ToArray());
|
||||
Assert.Equal(Array.Empty<string>(), ReviewedNotCredited.Keys.Where(path => credited.Contains(path)).ToArray());
|
||||
Assert.Equal(Array.Empty<string>(), UnlicensedWorks.Where(path => !Directory.Exists(Path.Combine(root, path))).ToArray());
|
||||
}
|
||||
|
||||
// Every holder the licence file states is in the credit that answers for it; the notices are typed into
|
||||
// Credits.cs, so this is what keeps them true to the files.
|
||||
[Fact]
|
||||
public void EveryCreditShowsTheHoldersItsOwnFileStates()
|
||||
{
|
||||
var credits = Load();
|
||||
Assert.Equal(Holders.Select(h => h.file).Append(Credits.NounProjectFile).OrderBy(f => f, StringComparer.Ordinal),
|
||||
credits.Select(credit => credit.LicenceFile).OrderBy(f => f, StringComparer.Ordinal));
|
||||
foreach (var (file, stated) in Holders)
|
||||
{
|
||||
var text = Normal(Read(file));
|
||||
var notice = Normal(credits.Single(credit => credit.LicenceFile == file).Notice);
|
||||
foreach (var holder in stated)
|
||||
{
|
||||
Assert.True(text.Contains(Normal(holder)), $"{file} does not state '{holder}'");
|
||||
Assert.True(notice.Contains(Normal(holder)), $"the credit for {file} omits '{holder}'");
|
||||
}
|
||||
// And every copyright line the file carries, whatever the table above says.
|
||||
foreach (var line in Read(file).Split('\n'))
|
||||
{
|
||||
var match = CopyrightLine.Match(line);
|
||||
if (!match.Success) continue;
|
||||
var body = Regex.Replace(match.Groups[1].Value.Replace("[", "").Replace("]", "").Trim(), @"(\s*All rights reserved)?\.?$", "", RegexOptions.IgnoreCase);
|
||||
Assert.True(notice.Contains(Normal(body)), $"the credit for {file} omits its copyright line '{body}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex CopyrightLine = new Regex(@"copyright\s*(?:\(c\)|\u00a9)?\s*(\[?\d.*)$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
// Dash variants and case folded, so the file's en dash and the screen's hyphen compare equal.
|
||||
private static string Normal(string text) => text.Replace('\u2013', '-').Replace('\u2014', '-').Replace("\r", "").ToLowerInvariant();
|
||||
|
||||
[Fact]
|
||||
public void TwemojiComesFirst()
|
||||
{
|
||||
var twemoji = Load()[0];
|
||||
Assert.Equal("Twemoji", twemoji.Name);
|
||||
Assert.Contains("CC BY 4.0", twemoji.Licence);
|
||||
Assert.Contains("Jason Sofonia & Justine De Caires", Read(Credits.TwemojiFile));
|
||||
Assert.Contains("Jason Sofonia & Justine De Caires", twemoji.Notice);
|
||||
Assert.Contains("Twitter", twemoji.Notice);
|
||||
}
|
||||
|
||||
// The expectations are stated here, not derived from the parser: 81 icon credits between the heading and the
|
||||
// "Other" sub-heading's two artists, named at the ends and at the line that was once split across two.
|
||||
[Fact]
|
||||
public void EveryIconCreditInTheAttributionFileIsShown()
|
||||
{
|
||||
var lines = Credits.NounProjectLines(Read(Credits.NounProjectFile)).ToArray();
|
||||
Assert.True(lines.Length > 50);
|
||||
var text = Read(Credits.NounProjectFile);
|
||||
Assert.StartsWith(Credits.NounProjectHeading, text.TrimStart('\uFEFF'));
|
||||
var lines = Credits.NounProjectLines(text).ToArray();
|
||||
Assert.Equal(82, lines.Length);
|
||||
Assert.Equal("Tornado by Bart Laubsch", lines[0]);
|
||||
Assert.Equal("Alien Abduction by Bart Laubsch", lines[1]);
|
||||
Assert.Equal("Hurricane by icon 54 from Smashicons Space by Ben Davis", lines[2]);
|
||||
Assert.Equal("Space 2 - Solid by Ben Davis", lines[3]);
|
||||
Assert.Contains("Other", lines);
|
||||
Assert.Equal("Missile War Weapon by Freepik", lines[^1]);
|
||||
// One credit per line: every line but the sub-heading names an artist and does not stop mid-sentence.
|
||||
foreach (var line in lines.Where(line => line != "Other"))
|
||||
{
|
||||
Assert.Contains(" by ", line, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotMatch(@"\s(from|by)$", line);
|
||||
}
|
||||
var notice = Load().Single(credit => credit.LicenceFile == Credits.NounProjectFile).Notice.Split('\n');
|
||||
Assert.Equal(lines, notice);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnAttributionFileWithoutItsHeadingIsRefused()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => Credits.NounProjectLines("Tornado by Bart Laubsch\nAlien Abduction by Bart Laubsch").ToArray());
|
||||
Assert.Throws<InvalidOperationException>(() => Credits.NounProjectLines("Noun Project\n\n").ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EntriesNameTheirLicenceAndHolder()
|
||||
{
|
||||
|
|
@ -63,8 +174,6 @@ namespace Aetheria.Shared.Tests
|
|||
Assert.False(string.IsNullOrWhiteSpace(credit.Licence));
|
||||
Assert.False(string.IsNullOrWhiteSpace(credit.Notice));
|
||||
}
|
||||
Assert.Contains("Playdead", Read(Credits.PlaydeadFile));
|
||||
Assert.Contains("Playdead", Load().Single(credit => credit.LicenceFile == Credits.PlaydeadFile).Notice);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -73,17 +182,20 @@ namespace Aetheria.Shared.Tests
|
|||
Assert.Throws<InvalidOperationException>(() => Credits.Validate(new[] { new Credit { Name = "x", Licence = "", Notice = "n", LicenceFile = "f" } }));
|
||||
}
|
||||
|
||||
// The Unity menu is not compilable headlessly, so this reads its source: the main menu must offer Credits and
|
||||
// the Credits screen must render the Shared list. It proves the wiring exists, not that the screen draws.
|
||||
// The Unity menu is not compilable headlessly, so this reads its source with comments removed (a commented-out
|
||||
// button or call must not satisfy it): the main menu must offer Credits, and ShowCredits must build the Shared
|
||||
// list. It proves the wiring exists, not that the screen draws.
|
||||
[Fact]
|
||||
public void MainMenuOffersACreditsPathThatRendersTheList()
|
||||
{
|
||||
var menu = Read("Assets/Scripts/UI/MainMenu.cs");
|
||||
var menu = Regex.Replace(Read("Assets/Scripts/UI/MainMenu.cs"), @"/\*.*?\*/|//[^\n]*", "", RegexOptions.Singleline);
|
||||
var main = menu.Substring(menu.IndexOf("private void ShowMain()", StringComparison.Ordinal));
|
||||
main = main.Substring(0, main.IndexOf("private void ShowScenarios()", StringComparison.Ordinal));
|
||||
main = main.Substring(0, main.IndexOf("private void ShowCredits()", StringComparison.Ordinal));
|
||||
Assert.Contains("AddButton(\"Credits\"", main);
|
||||
Assert.Contains("ShowCredits()", main);
|
||||
Assert.Contains("Credits.All(", menu);
|
||||
var credits = menu.Substring(menu.IndexOf("private void ShowCredits()", StringComparison.Ordinal));
|
||||
credits = credits.Substring(0, credits.IndexOf("private void ShowScenarios()", StringComparison.Ordinal));
|
||||
Assert.Contains("Credits.All(", credits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue