using System; using System.IO; using System.Linq; using System.Numerics; using FluentAssertions; using PerfectNumbers.Core.ByDivisor; using Xunit; using System.Text.Json; namespace PerfectNumbers.Core.Tests; public sealed class ByDivisorClassModelTests { [Fact] public void LoadOrBuild_builds_model_from_results_file() { string tempPath = Path.GetTempFileName(); try { File.WriteAllLines( tempPath, new[] { "p,divisor,searchedMersenne,detailedCheck,passedAllTests", "5,31,true,true,false", "5,0,true,true,true", "13,8191,true,true,false", }); string modelPath = tempPath + ".model"; ByDivisorScanTuning tuning = new() { CheapLimit = 32, }; ByDivisorClassModel model = ByDivisorClassModelLoader.LoadOrBuild(modelPath, tempPath, tuning, tuning.CheapLimit); ByDivisorClassEntry classFive = model.Entries[5]; classFive.K50.Should().Be(3d); classFive.K75.Should().Be(3d); classFive.GapProbability.Should().BeApproximately(0.5d, 1e-12); classFive.SampleCount.Should().Be(2); classFive.GapCount.Should().Be(1); ByDivisorClassEntry classThirteen = model.Entries[13]; classThirteen.K50.Should().Be(315d); classThirteen.K75.Should().Be(315d); classThirteen.GapProbability.Should().Be(0d); } finally { TryDelete(tempPath); TryDelete(tempPath + ".model"); } } [Fact] public void BuildPlan_uses_aggressive_tuning_for_hard_class() { var model = new ByDivisorClassModel { CheapLimit = 50, }; model.Entries[0] = new ByDivisorClassEntry { K50 = 1000d, K75 = 2000d, GapProbability = 0.8d, Stability = "escaping", }; ByDivisorScanTuning tuning = new() { CheapLimit = 50, Gamma50 = 0.8d, Gamma75 = 1.2d, AggressiveGamma50 = 1.0d, AggressiveGamma75 = 1.4d, Rho1 = 1.35d, Rho2 = 1.6d, AggressiveRho2 = 1.8d, HardGapThreshold = 0.75d, }; ByDivisorScanPlan plan = model.BuildPlan(2UL, BigInteger.Zero, tuning); plan.CheapLimit.Should().Be(50d); plan.Start.Should().Be(1000d); plan.Target.Should().Be(2800d); plan.Rho1.Should().Be(1.35d); plan.Rho2.Should().Be(1.8d); } [Fact] public void BuildPlan_exposes_percentiles_and_additive_delta() { var model = new ByDivisorClassModel { CheapLimit = 10_000, }; var entries = new ByDivisorClassEntry[1024]; entries[0] = new ByDivisorClassEntry { K10 = 10d, K25 = 20d, K50 = 30d, K75 = 40d, GapProbability = 0.1d, Stability = "stable", }; model.Entries = entries; ByDivisorScanTuning tuning = ByDivisorScanTuning.Default; ByDivisorScanPlan plan = model.BuildPlan(0UL, BigInteger.Zero, tuning); plan.K10.Should().Be(10d); plan.K25.Should().Be(20d); plan.K50.Should().Be(30d); plan.K75.Should().Be(40d); plan.Start.Should().Be(24d); // ceil(0.8 * 30) plan.Target.Should().Be(48d); // ceil(1.2 * 40) plan.DeltaK.Should().Be(4d); // ceil(24 * 0.15) } [Fact] public void BuildPlan_clamps_all_ranges_to_allowed_max() { var model = new ByDivisorClassModel { CheapLimit = 1000d, }; var entries = new ByDivisorClassEntry[1024]; entries[0] = new ByDivisorClassEntry { K10 = 50d, K25 = 60d, K50 = 70d, K75 = 80d, GapProbability = 0.1d, }; model.Entries = entries; ByDivisorScanTuning tuning = ByDivisorScanTuning.Default; // allowedMax = 41 => maxKAllowed = (41-1)/(2*5) = 4 ByDivisorScanPlan plan = model.BuildPlan(5UL, new BigInteger(41), tuning); plan.CheapLimit.Should().Be(4d); plan.K10.Should().Be(4d); plan.K25.Should().Be(4d); plan.K50.Should().Be(4d); plan.K75.Should().Be(4d); plan.Start.Should().Be(4d); plan.Target.Should().Be(4d); plan.DeltaK.Should().Be(4d); } [Fact] public void LoadOrBuild_rebuilds_when_results_newer_than_model() { string tempResults = Path.GetTempFileName(); string tempModel = tempResults + ".model"; try { File.WriteAllLines( tempResults, new[] { "p,divisor,searchedMersenne,detailedCheck,passedAllTests", "5,31,true,true,false", }); // Write a stale model var staleModel = new ByDivisorClassModel { CheapLimit = 10, }; File.WriteAllText(tempModel, JsonSerializer.Serialize(staleModel)); File.SetLastWriteTimeUtc(tempModel, DateTime.UtcNow.AddMinutes(-10)); File.SetLastWriteTimeUtc(tempResults, DateTime.UtcNow); ByDivisorScanTuning tuning = new() { CheapLimit = 32, }; ByDivisorClassModel model = ByDivisorClassModelLoader.LoadOrBuild(tempModel, tempResults, tuning, tuning.CheapLimit); model.Entries[5].K50.Should().Be(3d); File.GetLastWriteTimeUtc(tempModel).Should().BeAfter(File.GetLastWriteTimeUtc(tempResults).AddSeconds(-2)); } finally { TryDelete(tempResults); TryDelete(tempModel); } } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { // ignore cleanup failure in tests } } }