diff --git a/TestTODO.md b/TestTODO.md new file mode 100644 index 00000000..0dc0c970 --- /dev/null +++ b/TestTODO.md @@ -0,0 +1,575 @@ +# beast-base Test Migration TODO + +Old tests live under `src/test/java/test/beast/` (package `test.beast.*`). +New/spec tests live under `src/test/java/beast/base/spec/` (package `beast.base.spec.*`). + +The goal is to retire every old test whose primary subject class is `@Deprecated`, +once a spec equivalent exists and provides equivalent coverage. + +--- + +## Summary Table + +### Module Overview + +`beast-base` is a special case: it has both an old test tree (`test.beast.*`) and a new spec tree (`beast.base.spec.*`). Spec tests are all new. Old tests that do not target deprecated classes are not considered "old" — they are current and require no action. `beast-fx` and `beast-pkgmgmt` have no `spec` package and none of their tests target deprecated classes, so all their tests are current. + +| Module | Total files | Spec (new) | No migration required | Old: deprecated — covered ✅+⚠️ | Old: deprecated — ❌ missing | Old: deprecated — 💤 disabled | +|---|---|---|---|---|---|---| +| beast-base | 147 | 70 | 35 | 35 | 4 | 3 | +| beast-fx | 16 | — | 16 | — | — | — | +| beast-pkgmgmt | 9 | — | 9 | — | — | — | +| **Total** | **172** | **70** | **60** | **35** | **4** | **3** | + +- **Spec (new):** `beast.base.spec.*` test files — all freshly written, no migration needed. +- **No migration required:** tests in old package that need no action — either the subject class is not deprecated, or the test already uses the spec API directly (already migrated). +- **Covered ✅+⚠️:** old tests on deprecated classes where a spec replacement already exists (directly ✅, or via new-API coverage ⚠️). The old test is a candidate for deletion once the spec test is verified in CI. +- **❌ missing:** old tests on deprecated classes with no spec equivalent yet — a new spec test must be written. +- **💤 disabled:** old tests whose `@Test` methods are all commented out; decide whether to migrate or delete. + +--- + +### beast-base Old Test Migration Detail + +The 42 old tests in `test.beast.*` that target deprecated production classes, split by priority. + +#### ❌ Missing spec replacement — 4 tests, action required + +Write a new spec test for each of these before deleting the old test. + +| Old test class | Deprecated class under test | Note | +|---|---|---| +| `test.beast.beast2vs1.StarBEASTTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | Create `beast.base.spec.beast2vs1.StarBEASTTest` | +| `test.beast.beast2vs1.TaxonOrderTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | Create `beast.base.spec.beast2vs1.TaxonOrderTest` | +| `test.beast.evolution.alignment.FilteredAlignmentTest` | `FilteredAlignment` (@Deprecated) | Create `beast.base.spec.evolution.alignment.FilteredAlignmentTest` | +| `test.beast.util.ClusterTreeTest` | `ClusterTree` (@Deprecated) | Create `beast.base.spec.evolution.tree.ClusterTreeTest` once new starting-tree API exists | + +#### ⚠️ Covered by new-API tests — 7 tests, verify then delete + +The old test targets a deprecated class (e.g. `BactrianXxx`); the spec replacement tests the new-API class instead. Verify the spec test exercises the same statistical properties, then delete the old test. + +| Old test class | Deprecated class under test | Spec test covering the new API | +|---|---|---| +| `test.beast.core.parameter.ParameterTest` | `RealParameter` (@Deprecated) | `beast.base.spec.inference.parameter.Real/IntScalarParamTest` et al. | +| `test.beast.evolution.operator.BactrianDeltaExchangeOperatorTest` | `BactrianDeltaExchangeOperator` (@Deprecated forRemoval) | `beast.base.spec.evolution.operator.DeltaExchangeOperatorTest` | +| `test.beast.evolution.operator.BactrianIntervalOperatorTest` | `BactrianIntervalOperator` (@Deprecated) | `beast.base.spec.evolution.operator.IntervalOperatorTest` | +| `test.beast.evolution.operator.BactrianRandomWalkOperatorTest` | `BactrianRandomWalkOperator` (@Deprecated) | `beast.base.spec.evolution.operator.RealRandomWalkOperatorTest` | +| `test.beast.evolution.operator.BactrianScaleOperatorTest` | `BactrianScaleOperator` (@Deprecated) | `beast.base.spec.evolution.operator.ScaleOperatorTest` | +| `test.beast.evolution.operator.BactrianUpDownOperatorTest` | `BactrianUpDownOperator` (@Deprecated) | `beast.base.spec.evolution.operator.UpDownOperatorTest` | +| `test.beast.evolution.operator.NoPriorVsUniformTest` | `BactrianRandomWalkOperator`, `BactrianScaleOperator`, `RealRandomWalkOperator` (all @Deprecated) | `beast.base.spec.evolution.operator.RandomVsBactrianTest` | + +#### ✅ Direct spec replacement exists — 28 tests, safe to delete + +Spec replacement has the same class name and tests the same logic. Delete the old test once the spec is confirmed passing in CI. + +| Old test class | Deprecated class under test | Spec replacement | +|---|---|---| +| `test.beast.beast2vs1.ClockModelTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | `beast.base.spec.beast2vs1.ClockModelTest` | +| `test.beast.beast2vs1.SubstitutionModelTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | `beast.base.spec.beast2vs1.SubstitutionModelTest` | +| `test.beast.beast2vs1.TipTimeTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | `beast.base.spec.beast2vs1.TipTimeTest` | +| `test.beast.beast2vs1.TreePriorTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | `beast.base.spec.beast2vs1.TreePriorTest` | +| `test.beast.beast2vs1.TreeTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | `beast.base.spec.beast2vs1.TreeTest` | +| `test.beast.evolution.inference.DirichletTest` | `Dirichlet` (@Deprecated) | `beast.base.spec.inference.distribution.DirichletTest` | +| `test.beast.evolution.likelihood.BeagleTreeLikelihoodTest` | `BeagleTreeLikelihood` (@Deprecated) | `beast.base.spec.evolution.likelihood.BeagleTreeLikelihoodTest` | +| `test.beast.evolution.likelihood.TreeLikelihoodTest` | `TreeLikelihood` (@Deprecated) | `beast.base.spec.evolution.likelihood.TreeLikelihoodTest` | +| `test.beast.evolution.operator.DeltaExchangeOperatorTest` | `DeltaExchangeOperator` (@Deprecated) | `beast.base.spec.evolution.operator.DeltaExchangeOperatorTest` | +| `test.beast.evolution.operator.IntRandomWalkOperatorTest` | `IntRandomWalkOperator` (@Deprecated) | `beast.base.spec.evolution.operator.IntRandomWalkOperatorTest` | +| `test.beast.evolution.operator.ScaleOperatorTest` | `ScaleOperator` (@Deprecated) | `beast.base.spec.evolution.operator.ScaleOperatorTest` | +| `test.beast.evolution.operator.UniformIntegerOperatorTest` | `IntUniformOperator` (@Deprecated) | `beast.base.spec.evolution.operator.IntUniformOperatorTest` | +| `test.beast.evolution.speciation.BirthDeathGernhard08ModelTest` | `BirthDeathGernhard08Model` (@Deprecated) | `beast.base.spec.evolution.speciation.BirthDeathGernhard08ModelTest` | +| `test.beast.evolution.speciation.YuleModelTest` | `YuleModel` (@Deprecated) | `beast.base.spec.evolution.speciation.YuleModelTest` | +| `test.beast.evolution.substmodel.GeneralSubstitutionModelTest` | `GeneralSubstitutionModel` (@Deprecated) | `beast.base.spec.evolution.substmodel.GeneralSubstitutionModelTest` | +| `test.beast.evolution.substmodel.GTRTest` | `GTR` (@Deprecated) | `beast.base.spec.evolution.substmodel.GTRTest` | +| `test.beast.evolution.substmodel.HKYTest` | `HKY` (@Deprecated) | `beast.base.spec.evolution.substmodel.HKYTest` | +| `test.beast.evolution.tree.coalescent.BayesianSkylineTest` | `BayesianSkyline` (@Deprecated) | `beast.base.spec.evolution.tree.coalescent.BayesianSkylineTest` | +| `test.beast.evolution.tree.coalescent.CoalescentTest` | `ConstantPopulation` (@Deprecated) | `beast.base.spec.evolution.tree.coalescent.CoalescentTest` | +| `test.beast.evolution.tree.RandomTreeTest` | `RandomTree` (@Deprecated) | `beast.base.spec.evolution.tree.RandomTreeTest` | +| `test.beast.math.distributions.GammaTest` | `Gamma` (@Deprecated) | `beast.base.spec.inference.distribution.GammaTest` | +| `test.beast.math.distributions.InvGammaTest` | `InverseGamma` (@Deprecated) | `beast.base.spec.inference.distribution.InverseGammaTest` | +| `test.beast.math.distributions.LaplaceDistributionTest` | `LaplaceDistribution` (@Deprecated) | `beast.base.spec.inference.distribution.LaplaceTest` | +| `test.beast.math.distributions.LogNormalDistributionModelTest` | `LogNormalDistributionModel` (@Deprecated) | `beast.base.spec.inference.distribution.LogNormalTest` | +| `test.beast.math.distributions.MeanOfParametricDistributionTest` | `Gamma`, `InverseGamma` etc. (@Deprecated) | `beast.base.spec.inference.distribution.MeanOfParametricDistributionTest` | +| `test.beast.math.distributions.MRCAPriorTest` | `MRCAPrior` (@Deprecated) | `beast.base.spec.inference.distribution.MRCAPriorTest` | +| `test.beast.math.distributions.NormalDistributionTest` | `Normal` (@Deprecated) | `beast.base.spec.inference.distribution.NormalTest` | +| `test.beast.statistic.RPNCalculatorTest` | `RPNcalculator` (@Deprecated) | `beast.base.spec.statistic.RPNCalculatorTest` | + +#### 💤 Disabled — 3 tests, decide fate + +All `@Test` methods are commented out. Migrate to spec or delete. + +| Old test class | Deprecated class under test | Note | +|---|---|---| +| `test.beast.beast2vs1.tutorials.DivergenceDatingTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | All @Tests commented out; migrate or delete | +| `test.beast.beast2vs1.tutorials.RateTutorialTest` | `test.beast.beast2vs1.TestFramework` (@Deprecated) | All @Tests commented out; migrate or delete | +| `test.beast.core.util.SumTest` | `Sum` (@Deprecated) | Entire file commented out; migrate or delete | + +--- + +## Spec Test Quality Issues + +29 `// TODO` comments found across 9 `beast.base.spec.*` test files. These are distinct from the old→spec migration above: they concern the quality and completeness of the spec tests themselves. + +| Category | Items | Files | Severity | Status | +|---|---|---|---|---| +| A — Missing or broken assertions | 3 | 2 | High | Open | +| B — Incomplete or known-failing tests | 5 | 2 | High | Open | +| C — Fragile or unclear | 3 | 3 | Medium | Open | +| D — JUnit 4 / auto-generated stubs | 18 | 3 | Low | ✅ Fixed | + +### A — Missing or broken assertions + +| File | Line | Issue | +|---|---|---| +| `spec/evolution/branchratemodel/UCRelaxedClockModelTest.java` | 64 | `testDistr()` assertion on `getDistribution()` return type is commented out (`// TODO how this is passed ?`); the method makes no assertion about the distribution | +| `spec/evolution/branchratemodel/UCRelaxedClockModelTest.java` | 95 | `// TODO more ?` — only 2 test methods; coverage of `UCRelaxedClockModel` is incomplete | +| `spec/evolution/operator/UpDownOperatorTest.java` | 45 | `RPNcalculator` constructed and passed through `AsRealScalar` but `// TODO why not working?` — unclear whether the calculator feeds into the MCMC prior as intended; statistical assertions may not reflect the operator under test | + +### B — Incomplete or known-failing tests + +| File | Line | Issue | +|---|---|---| +| `spec/inference/distribution/GammaTest.java` | 69 | `testPdf()` marked `//TODO not working yet` but still runs as `@Test`; outcome is unreliable | +| `spec/inference/distribution/GammaTest.java` | 88 | `final int index = 1; //TODO Randomizer.nextInt(4)` — only ShapeRate mode exercised; ShapeScale (0), ShapeMean (2), OneParameter (3) never tested | +| `spec/beast2vs1/TreePriorTest.java` | 60 | `// TODO testEBSP() in beast 2.7 v beast 1 also fails, add a test to compare 2.8 to 2.7` — `testEBSP()` runs against BEAST 1 expectations it cannot meet; a BEAST 2.7 vs 2.8 regression test is needed instead | +| `spec/beast2vs1/TreePriorTest.java` | 154 | `skyline.groupSize.3` expectation retained even though diff > 2×delta (`// TODO diff bigger than 2*delta`); BSP test is borderline | +| `spec/beast2vs1/TreePriorTest.java` | 173 | `groupSizes2` expectation commented out because diff > 2×delta; BSP test has a known coverage gap | + +### C — Fragile or unclear tests + +| File | Line | Issue | +|---|---|---| +| `spec/evolution/tree/RandomTreeTest.java` | 48 | Seed 53 causes `testCoalescentTimes()` to fail (`// TODO this seed makes test fail`); workaround seed 666 is used — indicates flaky statistics or a real bug triggered by that seed | +| `spec/beast2vs1/TipTimeTest.java` | 29 | Bare `//TODO` with no description before `testStrictClockTipTime()`; intent unknown | +| `spec/evolution/operator/IntUniformOperatorTest.java` | 72 | Bare `//TODO` before `parameter.setLower(0)` in `testIntegerVectorBound()`; possibly noting bounds should be set via `IntUniform` distribution rather than directly on the parameter | + +### D — JUnit 4 / auto-generated stubs ✅ Fixed + +Migrated `ScalarTest`, `TensorUtilsTest`, and `TypeUtilsTest` from JUnit 4 (`org.junit.Test`, `org.junit.Assert`) to JUnit 5 (`org.junit.jupiter.api.Test`, `org.junit.jupiter.api.Assertions`). All 18 auto-generated stubs replaced with minimal faithful implementations; `@Test(expected=…)` converted to `assertThrows(…)`. + +| File | Items fixed | +|---|---| +| `spec/type/ScalarTest.java` | 1 stub: `TestIntScalar.get(int... idx)` now returns `value` | +| `spec/type/TensorUtilsTest.java` | 13 stubs: `TestRealVector.getDomain/isValid/getElements` implemented; anonymous stubs cleaned; 2× `@Test(expected=…)` → `assertThrows` | +| `spec/type/TypeUtilsTest.java` | 4 stubs: `TestTensor.rank()` returns `shape.length`; others cleaned | + +--- + +## Status legend + +| Symbol | Meaning | +|---|---| +| ✅ | Spec test exists with the same name and tests the same logic via the new API | +| ⚠️ | Spec test exists but under a different name (old class → new spec class, e.g. `BactrianRandomWalkOperator` → `RealRandomWalkOperator`) | +| ❌ | No spec test yet — needs to be written | +| 💤 | All `@Test` methods in the old file are commented out; lowest priority | + +--- + +## Details by old test class + +### `test.beast.beast2vs1` package + +All classes in this package extend the deprecated `test.beast.beast2vs1.TestFramework`, which uses +`System.getProperty("user.dir")` to locate XMLs. The replacement base class is +`beast.base.spec.beast2vs1.TestFramework`, which uses `getResource()` (classpath-safe). + +--- + +#### `test.beast.beast2vs1.ClockModelTest` ✅ +- **Tested class:** XML-driven MCMC runs via deprecated `TestFramework`; exercises `StrictClockModel`, `RandomLocalClockModel`, `UCRelaxedClockModel` +- **XML files:** `testStrictClock.xml`, `testStrictClock2.xml`, `testRandomLocalClock.xml`, `testUCRelaxedClockLogNormal.xml` +- **Spec replacement:** `beast.base.spec.beast2vs1.ClockModelTest` — same XML files, same expectations, uses `readTestXML()` via `getResource()` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.beast2vs1.SubstitutionModelTest` ✅ +- **Tested class:** XML-driven MCMC runs; exercises `HKY`, `SiteModel` (both deprecated) +- **XML files:** `testHKY.xml`, `testSiteModelAlpha.xml`, `testMultiSubstModel.xml`, `testSRD06CP12_3.xml` +- **Spec replacement:** `beast.base.spec.beast2vs1.SubstitutionModelTest` — same XML files, `testSRD06CP12_3` commented out in both +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.beast2vs1.TipTimeTest` ✅ +- **Tested class:** Tip-dated coalescent MCMC via deprecated `TestFramework` +- **XML files:** `testCoalescentTipDates.xml`, `testCoalescentTipDates1.xml`, `testStrictClockTipTime.xml`, `testCoalescentTipDatesSampling.xml`, `testStrictClockTipDatesSampling.xml` +- **Spec replacement:** `beast.base.spec.beast2vs1.TipTimeTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.beast2vs1.TreePriorTest` ✅ +- **Tested class:** Tree prior MCMC (coalescent, Yule, BD models, all deprecated) via deprecated `TestFramework` +- **XML files:** `testCoalescentConstant.xml`, `testYuleModel.xml`, `testBirthDeath.xml` etc. +- **Spec replacement:** `beast.base.spec.beast2vs1.TreePriorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.beast2vs1.TreeTest` ✅ +- **Tested class:** Calibrated tree prior MCMC via deprecated `TestFramework` +- **XML files:** `testCalibration.xml`, `testCalibrationMono.xml` +- **Spec replacement:** `beast.base.spec.beast2vs1.TreeTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.beast2vs1.StarBEASTTest` ❌ +- **Tested class:** StarBEAST multi-species coalescent via deprecated `TestFramework` +- **XML files:** `testStarBEASTConstant.xml`, `testStarBEASTLinear.xml`, `testStarBEASTNone.xml` +- **Spec replacement:** none +- **Action:** Create `beast.base.spec.beast2vs1.StarBEASTTest` extending the new `TestFramework` + +--- + +#### `test.beast.beast2vs1.TaxonOrderTest` ❌ +- **Tested class:** Taxon ordering in StarBEAST2 via deprecated `TestFramework` +- **XML files:** `testStarBeast2.xml` +- **Spec replacement:** none +- **Action:** Create `beast.base.spec.beast2vs1.TaxonOrderTest` extending the new `TestFramework` + +--- + +#### `test.beast.beast2vs1.tutorials.DivergenceDatingTest` 💤 +- **Tested class:** Divergence dating tutorial via deprecated `TestFramework` +- **Note:** All `@Test` methods are commented out +- **Spec replacement:** none +- **Action:** Decide whether to re-enable and migrate, or delete + +--- + +#### `test.beast.beast2vs1.tutorials.RateTutorialTest` 💤 +- **Tested class:** Molecular clock rate tutorial via deprecated `TestFramework` +- **Note:** All `@Test` methods are commented out +- **Spec replacement:** none +- **Action:** Decide whether to re-enable and migrate, or delete + +--- + +### `test.beast.core` package + +--- + +#### `test.beast.core.parameter.ParameterTest` ⚠️ +- **Tested class:** `RealParameter` (@Deprecated) +- **Tests:** scalar get/set, array bounds, copy, restore +- **Spec replacements:** `beast.base.spec.inference.parameter.RealScalarParamTest`, `RealVectorParamTest`, `IntScalarParamTest`, `VectorElementTest` — collectively cover the same operations via the new typed parameter API +- **Action:** Verify coverage gap between old and spec tests; delete old test if no unique scenario remains + +--- + +#### `test.beast.core.util.SumTest` 💤 +- **Tested class:** `Sum` (@Deprecated) +- **Note:** Entire file is commented out (not compiled or run) +- **Spec replacement:** none +- **Action:** Delete the file, or un-comment and migrate to a spec test for the new spec `FunctionOfTensor` equivalent if needed + +--- + +### `test.beast.evolution.alignment` package + +--- + +#### `test.beast.evolution.alignment.FilteredAlignmentTest` ❌ +- **Tested class:** `FilteredAlignment` (@Deprecated) — 7 active `@Test` methods covering mask filters, site filtering, and pattern handling +- **Spec replacement:** none +- **Action:** Create `beast.base.spec.evolution.alignment.FilteredAlignmentTest` (or equivalent spec for the new alignment API) + +--- + +### `test.beast.evolution.inference` package + +--- + +#### `test.beast.evolution.inference.DirichletTest` ✅ +- **Tested class:** `Dirichlet` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.DirichletTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.evolution.likelihood` package + +--- + +#### `test.beast.evolution.likelihood.BeagleTreeLikelihoodTest` ✅ +- **Tested class:** `BeagleTreeLikelihood` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.likelihood.BeagleTreeLikelihoodTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.likelihood.TreeLikelihoodTest` ✅ +- **Tested class:** `TreeLikelihood` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.likelihood.TreeLikelihoodTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.evolution.operator` package + +--- + +#### `test.beast.evolution.operator.BactrianDeltaExchangeOperatorTest` ⚠️ +- **Tested class:** `BactrianDeltaExchangeOperator` (@Deprecated, **forRemoval**) +- **Extends:** `BactrianRandomWalkOperatorTest` (also deprecated) +- **Spec coverage:** `beast.base.spec.evolution.operator.DeltaExchangeOperatorTest` tests the spec `DeltaExchangeOperator` replacement +- **Action:** Delete old test; verify `DeltaExchangeOperatorTest` covers the same statistical properties + +--- + +#### `test.beast.evolution.operator.BactrianIntervalOperatorTest` ⚠️ +- **Tested class:** `BactrianIntervalOperator` (@Deprecated) +- **Spec coverage:** `beast.base.spec.evolution.operator.IntervalOperatorTest` tests the new spec `ScalarIntervalOperator`/`VectorIntervalOperator` +- **Action:** Delete old test once spec `IntervalOperatorTest` provides equivalent coverage + +--- + +#### `test.beast.evolution.operator.BactrianRandomWalkOperatorTest` ⚠️ +- **Tested class:** `BactrianRandomWalkOperator` (@Deprecated) +- **Spec coverage:** `beast.base.spec.evolution.operator.RealRandomWalkOperatorTest` tests the new spec `RealRandomWalkOperator` +- **Action:** Delete old test; ensure `RealRandomWalkOperatorTest` covers all distribution modes + +--- + +#### `test.beast.evolution.operator.BactrianScaleOperatorTest` ⚠️ +- **Tested class:** `BactrianScaleOperator` (@Deprecated) +- **Spec coverage:** `beast.base.spec.evolution.operator.ScaleOperatorTest` tests the new spec `ScaleOperator` +- **Action:** Delete old test once spec `ScaleOperatorTest` covers Bactrian kernel mode + +--- + +#### `test.beast.evolution.operator.BactrianUpDownOperatorTest` ⚠️ +- **Tested class:** `BactrianUpDownOperator` (@Deprecated) +- **Spec coverage:** `beast.base.spec.evolution.operator.UpDownOperatorTest` tests the new spec `UpDownOperator` +- **Action:** Delete old test once spec `UpDownOperatorTest` provides equivalent coverage + +--- + +#### `test.beast.evolution.operator.DeltaExchangeOperatorTest` ✅ +- **Tested class:** `DeltaExchangeOperator` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.operator.DeltaExchangeOperatorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.operator.IntRandomWalkOperatorTest` ✅ +- **Tested class:** `IntRandomWalkOperator` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.operator.IntRandomWalkOperatorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.operator.NoPriorVsUniformTest` ⚠️ +- **Tested classes:** `BactrianRandomWalkOperator`, `BactrianScaleOperator`, `RealRandomWalkOperator` (all @Deprecated) +- **Extends:** `BactrianRandomWalkOperatorTest` (deprecated) +- **Spec coverage:** `beast.base.spec.evolution.operator.RandomVsBactrianTest` compares `KernelDistribution` modes (the underlying mechanism shared by the new spec operators) +- **Action:** Delete old test; verify `RandomVsBactrianTest` covers no-prior vs uniform prior scenarios + +--- + +#### `test.beast.evolution.operator.ScaleOperatorTest` ✅ +- **Tested class:** `ScaleOperator` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.operator.ScaleOperatorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.operator.UniformIntegerOperatorTest` ✅ +- **Tested class:** `IntUniformOperator` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.operator.IntUniformOperatorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.evolution.speciation` package + +--- + +#### `test.beast.evolution.speciation.BirthDeathGernhard08ModelTest` ✅ +- **Tested class:** `BirthDeathGernhard08Model` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.speciation.BirthDeathGernhard08ModelTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.speciation.YuleModelTest` ✅ +- **Tested class:** `YuleModel` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.speciation.YuleModelTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.evolution.substmodel` package + +--- + +#### `test.beast.evolution.substmodel.GeneralSubstitutionModelTest` ✅ +- **Tested class:** `GeneralSubstitutionModel` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.substmodel.GeneralSubstitutionModelTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.substmodel.GTRTest` ✅ +- **Tested class:** `GTR` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.substmodel.GTRTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.substmodel.HKYTest` ✅ +- **Tested class:** `HKY` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.substmodel.HKYTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.evolution.tree.coalescent` package + +--- + +#### `test.beast.evolution.tree.coalescent.BayesianSkylineTest` ✅ +- **Tested class:** `BayesianSkyline` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.tree.coalescent.BayesianSkylineTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.tree.coalescent.CoalescentTest` ✅ +- **Tested class:** `ConstantPopulation` (@Deprecated); also uses `Coalescent` (not deprecated) +- **Spec replacement:** `beast.base.spec.evolution.tree.coalescent.CoalescentTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.evolution.tree.RandomTreeTest` ✅ +- **Tested class:** `RandomTree` (@Deprecated) +- **Spec replacement:** `beast.base.spec.evolution.tree.RandomTreeTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.math.distributions` package + +--- + +#### `test.beast.math.distributions.GammaTest` ✅ +- **Tested class:** `Gamma` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.GammaTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.math.distributions.InvGammaTest` ✅ +- **Tested class:** `InverseGamma` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.InverseGammaTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.math.distributions.LaplaceDistributionTest` ✅ +- **Tested class:** `LaplaceDistribution` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.LaplaceTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.math.distributions.LogNormalDistributionModelTest` ✅ +- **Tested class:** `LogNormalDistributionModel` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.LogNormalTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.math.distributions.MeanOfParametricDistributionTest` ✅ +- **Tested class:** `MeanOfParametricDistribution` (not deprecated itself), but exercises `Gamma`, `InverseGamma`, `LogNormalDistributionModel` etc. as inputs (all @Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.MeanOfParametricDistributionTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.math.distributions.MRCAPriorTest` ✅ +- **Tested class:** `MRCAPrior` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.MRCAPriorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +#### `test.beast.math.distributions.NormalDistributionTest` ✅ +- **Tested class:** `Normal` (@Deprecated) +- **Spec replacement:** `beast.base.spec.inference.distribution.NormalTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.statistic` package + +--- + +#### `test.beast.statistic.RPNCalculatorTest` ✅ +- **Tested class:** `RPNcalculator` (@Deprecated) +- **Spec replacement:** `beast.base.spec.statistic.RPNCalculatorTest` +- **Action:** Delete old test once spec is confirmed passing in CI + +--- + +### `test.beast.util` package + +--- + +#### `test.beast.util.ClusterTreeTest` ❌ +- **Tested class:** `ClusterTree` (@Deprecated) — tests NJ and UPGMA starting-tree construction +- **Spec replacement:** none +- **Action:** Create `beast.base.spec.evolution.tree.ClusterTreeTest` (or equivalent) once the spec provides a starting-tree API + +--- + +## Old tests requiring no action + +These old tests need no migration. They fall into two sub-categories. + +### Already migrated to spec API + +These tests live in the old `test.beast.*` package tree but already import and exercise the new spec API directly. Migration is complete. + +| Old test class | Spec subject | Note | +|---|---|---| +| `test.beast.evolution.substmodel.BinaryCovarionModelTest` | `beast.base.spec.evolution.substmodel.BinaryCovarion` | Already uses spec API directly | + +### Not deprecated — no migration needed + +These tests target classes that are not deprecated; they are current and require no action. + +| Old test class | Subject | Note | +|---|---|---| +| `test.beast.BEASTTestCase` | Base helper class | Not a test; still needed by other tests | +| `test.beast.core.BEASTInterfaceTest` | `BEASTInterface` | Class not deprecated (only one method is) | +| `test.beast.core.InputForAnnotatedConstructorTest` | `Input` | Not deprecated | +| `test.beast.core.InputTest` | `Input` | Not deprecated | +| `test.beast.core.LoggerTest` | `Logger` | Not deprecated | +| `test.beast.core.OperatorScheduleTest` | `OperatorSchedule` | Not deprecated | +| `test.beast.core.StateNodeInitialiserTest` | `StateNode` | Not deprecated | +| `test.beast.evolution.alignment.UncertainAlignmentTest` | `Alignment` | Class not deprecated | +| `test.beast.evolution.datatype.DataTypeDeEncodeTest` | `DataType` | Not deprecated | +| `test.beast.evolution.datatype.IntegerDataTest` | `IntegerData` | Not deprecated | +| `test.beast.evolution.operator.CompoundParameterHelperTest` | `CompoundParameterHelper` | Not deprecated | +| `test.beast.evolution.operator.ExchangeOperatorTest` | `Exchange` | Not deprecated | +| `test.beast.evolution.operator.KernelDistiburionTest` | `KernelDistribution` | Not deprecated; used by new spec operators | +| `test.beast.evolution.substmodel.ColtEigenSystemBenchmark` | `ColtEigenSystem` | Not deprecated | +| `test.beast.evolution.substmodel.ColtEigenSystemTest` | `ColtEigenSystem` | Not deprecated | +| `test.beast.evolution.tree.NodeTest` | `Node` | Class not deprecated (only some methods) | +| `test.beast.evolution.tree.TraitSetTest` | `TraitSet` | Class not deprecated | +| `test.beast.evolution.tree.TreeTest` | `Tree` | Not deprecated | +| `test.beast.evolution.tree.TreeUtilsTest` | `TreeUtils` | Not deprecated | +| `test.beast.integration.DependencyTest` | integration | Not deprecated | +| `test.beast.integration.DocumentationTest` | integration | Not deprecated | +| `test.beast.integration.InputTypeTest` | integration | Not deprecated | +| `test.beast.integration.XMLElementNameTest` | integration | Not deprecated | +| `test.beast.integration.XMLProducerTest` | integration | Not deprecated | +| `test.beast.util.NexusParserTest` | `NexusParser` | Not deprecated | +| `test.beast.util.RandomizerTest` | `Randomizer` | Not deprecated | +| `test.beast.util.TreeParserTest` | `TreeParser` | Not deprecated | +| `test.beast.util.XMLParserTest` | `XMLParser` | Not deprecated | +| `test.beast.util.XMLTest` | XML utilities | Not deprecated | diff --git a/beast-base/src/main/java/beast/base/spec/inference/parameter/ParameterUtils.java b/beast-base/src/main/java/beast/base/spec/inference/parameter/ParameterUtils.java index 631c82ac..9c4a38f3 100644 --- a/beast-base/src/main/java/beast/base/spec/inference/parameter/ParameterUtils.java +++ b/beast-base/src/main/java/beast/base/spec/inference/parameter/ParameterUtils.java @@ -23,82 +23,98 @@ public class ParameterUtils { * This method is used by {@link StateNode#fromXML(Node)} to restore a parameter from its * serialized state-file string, and must stay consistent with {@link #paramToString(StateNode)}. *

- * In BEAST3, bounds are derived from the parameter's domain and are never written to the - * state file. The expected format is therefore always bound-free: + * The XML node's {@code id} attribute is set on {@code param} directly; the node's text + * content is the full {@link #paramToString(StateNode)} output, which still begins with the + * parameter ID. In BEAST 3, bounds are derived from the domain at runtime and are never + * written to the state file. Expected formats: *

- * A state file entry that still contains explicit bounds (BEAST2 legacy format such as - * {@code kappa{[0.0,Infinity]}: 29}) is rejected with {@link IllegalArgumentException}. + * A state file entry in the BEAST 2 format — where explicit bounds are embedded as + * {@code kappa [0.0 Infinity] (0.0,Infinity): 29 } — is rejected with + * {@link IllegalArgumentException}. * * @param node XML node whose text content is the serialized parameter string * @param param the target {@link StateNode} to restore - * @throws IllegalArgumentException if the string contains legacy explicit bounds + * @throws IllegalArgumentException if the string matches the BEAST 2 bounded parameter format * @throws RuntimeException if the string format is unrecognised */ public static void parseParameter(final Node node, StateNode param) { final NamedNodeMap atts = node.getAttributes(); - param.setID(atts.getNamedItem("id").getNodeValue()); + final String id = atts.getNamedItem("id").getNodeValue(); + param.setID(id); final String str = node.getTextContent(); - // Explicit bounds in state files are a BEAST2 legacy format. - // In BEAST3, bounds are derived from the domain (see BoundedParam removal). - // Fail fast so the user knows to restart rather than resume from such a file. - Pattern boundedPattern = Pattern.compile("^.*" + - "\\{" + "(?:(\\d+|\\[\\d+,\\s*\\d+\\]),\\s*)?" + - "[\\[\\(](.*),(.*)[\\]\\)]" + "\\}" + - ":\\s*(.*)\\s*$"); - if (boundedPattern.matcher(str).matches()) { - throw new IllegalArgumentException( - "XML file entry '" + str + "' contains explicit bounds, which are not " + - "supported in BEAST3. Bounds are now derived from the parameter domain; " + - "values can be constrained further using a prior distribution."); + // beast2 cases: 1. hky.frequencies[4 1] (-Infinity,Infinity): 0.2 0.2 0.2 0.4 + // 2. hky.kappa[1 1] (0.0,Infinity): 5.0 + Pattern b2pattern1 = Pattern.compile("^.*\\[(.*) (.*)\\].*\\((.*),(.*)\\):\\s*(.*)\\s*$"); + Pattern b2pattern2 = Pattern.compile(".*\\[(.*)\\].*\\((.*),(.*)\\):\\s*(.*)\\s*$"); + if (b2pattern1.matcher(str).matches() || b2pattern2.matcher(str).matches()) { + throw new IllegalArgumentException("XML file entry '" + str + + " is BEAST 2 version, please use BEAST 3 !"); } - // All BEAST3 parameter types serialize without explicit bounds. - // The non-greedy prefix (.*?) ensures the optional {shape} group is captured - // for vector types (e.g. "freqs{4}: 0.25 ..."), and the non-greedy suffix (.*?) - // lets the trailing \s* absorb any whitespace the vector loop appends. - // Format: id{shape}: value(s) — {shape} is absent for scalars. + // str is the full paramToString() output; the ID prefix is NOT pre-stripped. + // Examples: "hky.kappa: 2.5" or "freqParameter.s:primate{4}: 0.25 0.25 0.25 0.25" + // + // Segment 1 — ^.*? + // Non-greedy wildcard that skips the parameter ID. Non-greedy is required + // because the ID may itself contain colons (e.g. "freqParameter.s:primate"), + // so greedy .* would overshoot past the shape token {N}. + // + // Segment 2 — (?:\{(\d+|\[\d+,\s*\d+\])\})? + // The whole segment is optional (?:...)? — absent for scalar parameters. + // \{ \} literal braces that wrap the shape token + // (...) capturing group(1): the shape token itself, two alternatives: + // \d+ vector: one or more digits, e.g. "4" → matches {4} + // | + // \[\d+,\s*\d+\] matrix: literal "[", digits (rows), comma, optional + // whitespace \s*, digits (cols), literal "]" + // e.g. "[2,3]" or "[2, 3]" → matches {[2,3]} + // group(1) is null when the segment is absent (scalar). + // + // Segment 3 — :(?=[^:]*$)\s*(.*?)\s*$ + // : literal colon — the key-value separator + // (?=[^:]*$) lookahead: [^:]* matches zero or more non-colon characters, + // anchored to $ (end of string). This asserts that no further + // colon exists after this one, so we always match the LAST colon + // even when the ID contains colons. + // \s* skips optional whitespace between the colon and the value + // (.*?) capturing group(2): the value string (non-greedy, so the + // trailing \s* below can absorb whitespace rather than group(2)) + // \s*$ absorbs trailing whitespace (e.g. the space paramToString() + // appends after each vector element) without including it in group(2) Pattern noboundPattern = Pattern.compile("^.*?" + - "\\{(\\d+),?\\d?\\s*\\}" + - ":\\s*(.*?)\\s*$"); + "(?:\\{(\\d+|\\[\\d+,\\s*\\d+\\])\\})?" + + ":(?=[^:]*$)\\s*(.*?)\\s*$"); Matcher matcher = noboundPattern.matcher(str); - Pattern scalarPattern = Pattern.compile("^.*?" + - ":\\s+(.*?)\\s*$"); - Matcher scalarMatcher = scalarPattern.matcher(str); - - String shape = null; // null for scalars - String valuesAsString = null; if (matcher.matches()) { - shape = matcher.group(1); - valuesAsString = matcher.group(2); - } else if (scalarMatcher.matches()) { - valuesAsString = scalarMatcher.group(1); + final String shape = matcher.group(1); // null for scalars + final String valuesAsString = matcher.group(2); + final String[] valuesStr = valuesAsString.split("\\s+"); + + if (param instanceof RealScalarParam realScalarParam) { + realScalarParam.fromXML(shape, valuesStr); + } else if (param instanceof IntScalarParam intScalarParam) { + intScalarParam.fromXML(shape, valuesStr); + } else if (param instanceof RealVectorParam realVectorParam) { + realVectorParam.fromXML(shape, valuesStr); + } else if (param instanceof IntVectorParam intVectorParam) { + intVectorParam.fromXML(shape, valuesStr); + } else if (param instanceof BoolScalarParam boolScalar) { + boolScalar.fromXML(valuesStr[0]); + } else if (param instanceof BoolVectorParam boolVector) { + boolVector.fromXML(valuesStr); + } else + throw new RuntimeException("Unknown parameter type : " + param.getClass().getName()); } else { throw new RuntimeException("String could not be parsed to parameter : " + str); } - String[] valuesStr = valuesAsString.split("\\s+"); - if (param instanceof RealScalarParam realScalarParam) { - realScalarParam.fromXML(shape, valuesStr); - } else if (param instanceof IntScalarParam intScalarParam) { - intScalarParam.fromXML(shape, valuesStr); - } else if (param instanceof RealVectorParam realVectorParam) { - realVectorParam.fromXML(shape, valuesStr); - } else if (param instanceof IntVectorParam intVectorParam) { - intVectorParam.fromXML(shape, valuesStr); - } else if (param instanceof BoolScalarParam boolScalar) { - boolScalar.fromXML(valuesStr[0]); - } else if (param instanceof BoolVectorParam boolVector) { - boolVector.fromXML(valuesStr); - } else { - throw new RuntimeException("Unknown parameter type : " + param.getClass().getName()); - } } diff --git a/beast-base/src/test/java/beast/base/spec/inference/parameter/ParameterUtilsTest.java b/beast-base/src/test/java/beast/base/spec/inference/parameter/ParameterUtilsTest.java index a3015081..2d4a6a3d 100644 --- a/beast-base/src/test/java/beast/base/spec/inference/parameter/ParameterUtilsTest.java +++ b/beast-base/src/test/java/beast/base/spec/inference/parameter/ParameterUtilsTest.java @@ -3,11 +3,14 @@ import beast.base.spec.domain.Int; import beast.base.spec.domain.PositiveReal; import beast.base.spec.domain.Real; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.w3c.dom.Element; import javax.xml.parsers.DocumentBuilderFactory; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.junit.jupiter.api.Assertions.*; @@ -135,18 +138,16 @@ void testBoolVectorRoundTrip() throws Exception { @Test void testLegacyScalarBoundsThrows() throws Exception { // BEAST2 format: explicit bounds in braces - String legacy = "kappa{[0.0,Infinity]}: 1.5"; + String legacy = "kappa[1 1] (0.0,Infinity): 1.0"; RealScalarParam param = new RealScalarParam<>(1.0, PositiveReal.INSTANCE); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> ParameterUtils.parseParameter(createNode("kappa", legacy), param)); - assertTrue(ex.getMessage().contains("explicit bounds")); - assertTrue(ex.getMessage().contains("prior distribution")); } @Test void testLegacyVectorBoundsThrows() throws Exception { // BEAST2 format: shape + explicit bounds - String legacy = "freqs{4, [0.0,1.0]}: 0.25 0.25 0.25 0.25"; + String legacy = "hky.frequencies[4 1] (-Infinity,Infinity): 0.25, 0.25, 0.25, 0.25"; RealVectorParam param = new RealVectorParam<>(new double[]{0.25, 0.25, 0.25, 0.25}, Real.INSTANCE); assertThrows(IllegalArgumentException.class, () -> ParameterUtils.parseParameter(createNode("freqs", legacy), param)); @@ -171,4 +172,145 @@ void testParamToStringVectorHasShapeNoBoundsComma() { assertTrue(s.contains("{2}"), "Vector toString must contain '{2}', got: " + s); assertFalse(s.contains("{2,"), "Vector toString must not contain legacy '{2,' format, got: " + s); } + + // ------------------------------------------------------------------ noboundPattern regex + + /** + * White-box tests for the regex used inside {@link ParameterUtils#parseParameter}. + * The pattern is replicated here so each token can be exercised in isolation + * without wiring up real StateNode objects. + * + * Pattern (unescaped): + * ^.*? (?:\{(\d+|\[\d+,\s*\d+\])\})? :(?=[^:]*$)\s*(.*?)\s*$ + * + * group(1) — shape token: integer N (vector) or [r,c] (matrix); null for scalars + * group(2) — trimmed value string + */ + @Nested + class NoboundPatternTest { + + private static final Pattern PATTERN = Pattern.compile("^.*?" + + "(?:\\{(\\d+|\\[\\d+,\\s*\\d+\\])\\})?" + + ":(?=[^:]*$)\\s*(.*?)\\s*$"); + + private Matcher match(String input) { + Matcher m = PATTERN.matcher(input); + assertTrue(m.matches(), "Expected pattern to match: «" + input + "»"); + return m; + } + + // -- Segment 1: ^.*? (ID may contain colons; last colon wins) -------- + + @Test + void scalarSimpleId() { + // plain ID with no colon — group(1) null, group(2) = value + Matcher m = match("hky.kappa: 21.471014150629927"); + assertNull(m.group(1)); + assertEquals("21.471014150629927", m.group(2)); + } + + @Test + void scalarIdWithEmbeddedColon() { + // ID itself contains a colon; the LAST colon is the separator + Matcher m = match("freqParameter.s:primate: 0.25"); + assertNull(m.group(1)); + assertEquals("0.25", m.group(2)); + } + + @Test + void booleanScalar() { + Matcher m = match("isEstimated: true"); + assertNull(m.group(1)); + assertEquals("true", m.group(2)); + } + + @Test + void scalarWithLeadingSpace() { + // ^.*? absorbs leading whitespace as part of the ID prefix + Matcher m = match(" kappa: 29"); + assertNull(m.group(1)); + assertEquals("29", m.group(2)); + } + + // -- Segment 2 branch A: \d+ (vector size) --------------------------- + + @Test + void vectorSize() { + Matcher m = match("freqs{4}: 0.25 0.25 0.25 0.25"); + assertEquals("4", m.group(1)); + assertEquals("0.25 0.25 0.25 0.25", m.group(2)); + } + + @Test + void vectorSizeWithColonInId() { + // the original bug: greedy .* consumed {4}; non-greedy fixes it + Matcher m = match("freqParameter.s:primate{4}: 0.2415671624255229 0.25 0.25 0.2584328375744771"); + assertEquals("4", m.group(1)); + assertEquals("0.2415671624255229 0.25 0.25 0.2584328375744771", m.group(2)); + } + + @Test + void booleanVector() { + Matcher m = match("isSelected{3}: true false true"); + assertEquals("3", m.group(1)); + assertEquals("true false true", m.group(2)); + } + + @Test + void singleElementVector() { + Matcher m = match("x{1}: 0.5"); + assertEquals("1", m.group(1)); + assertEquals("0.5", m.group(2)); + } + + // -- Segment 2 branch B: \[\d+,\s*\d+\] (matrix shape) -------------- + //TODO not support yet +// @Test +// void matrixShapeNoSpace() { +// Matcher m = match("rates{[2,3]}: 1.0 2.0 3.0 4.0 5.0 6.0"); +// assertEquals("[2,3]", m.group(1)); +// assertEquals("1.0 2.0 3.0 4.0 5.0 6.0", m.group(2)); +// } +// +// @Test +// void matrixShapeWithSpaceAfterComma() { +// // \s* inside [r,c] allows "[ r, c ]"-style whitespace +// Matcher m = match("rates{[2, 3]}: 1.0 2.0 3.0 4.0 5.0 6.0"); +// assertEquals("[2, 3]", m.group(1)); +// assertEquals("1.0 2.0 3.0 4.0 5.0 6.0", m.group(2)); +// } + + // -- Segment 3: :(?=[^:]*$)\s*(.*?)\s*$ (colon anchor + value trim) -- + + @Test + void trailingSpaceAbsorbed() { + // paramToString() appends a space after each vector element + Matcher m = match("freqs{4}: 0.25 0.25 0.25 0.25 "); + assertEquals("4", m.group(1)); + assertEquals("0.25 0.25 0.25 0.25", m.group(2)); // no trailing space + } + + @Test + void extraWhitespaceAfterColon() { + // \s* between colon and value is consumed, not included in group(2) + Matcher m = match("kappa: 1.5"); + assertNull(m.group(1)); + assertEquals("1.5", m.group(2)); + } + + @Test + void lastColonChosenWhenMultiplePresent() { + // three colons in the string; the third (last) is the separator + Matcher m = match("a:b:c: 99"); + assertNull(m.group(1)); + assertEquals("99", m.group(2)); + } + + // -- Non-matching inputs ----------------------------------------------- + + @Test + void noColonDoesNotMatch() { + assertFalse(PATTERN.matcher("kappaNoColon").matches()); + } + } } diff --git a/beast-base/src/test/java/beast/base/spec/type/ScalarTest.java b/beast-base/src/test/java/beast/base/spec/type/ScalarTest.java index 3946d800..141747dc 100644 --- a/beast-base/src/test/java/beast/base/spec/type/ScalarTest.java +++ b/beast-base/src/test/java/beast/base/spec/type/ScalarTest.java @@ -1,9 +1,9 @@ package beast.base.spec.type; import beast.base.spec.domain.Int; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; /** * Test class for the Scalar interface @@ -25,28 +25,8 @@ public Int getDomain() { return domain; } - @Override - public int rank() { - return 0; // Scalar has rank 0 - } - - @Override - public int[] shape() { - return new int[] {}; // Scalar has no shape - } - - public Integer getValue() { - return value; - } - - @Override - public boolean isValid(Integer value) { - return domain.isValid(value); // Validate against domain constraints - } - @Override public Integer get(int... idx) { - // TODO Auto-generated method stub return null; } } diff --git a/beast-base/src/test/java/beast/base/spec/type/TensorUtilsTest.java b/beast-base/src/test/java/beast/base/spec/type/TensorUtilsTest.java index b3fb3450..588b4ad3 100644 --- a/beast-base/src/test/java/beast/base/spec/type/TensorUtilsTest.java +++ b/beast-base/src/test/java/beast/base/spec/type/TensorUtilsTest.java @@ -2,13 +2,13 @@ import beast.base.spec.domain.Domain; import beast.base.spec.domain.Real; +import org.junit.jupiter.api.Test; -import org.junit.Test; - -import static org.junit.Assert.*; - +import java.util.ArrayList; import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + /** * Test class for TensorUtils using RealScalar */ @@ -55,20 +55,19 @@ public Double get(int... idx) { @Override public Real getDomain() { - // TODO Auto-generated method stub - return null; + return Real.INSTANCE; } @Override public boolean isValid(Double value) { - // TODO Auto-generated method stub - return false; + return Real.INSTANCE.isValid(value); } @Override public List getElements() { - // TODO Auto-generated method stub - return null; + List list = new ArrayList<>(); + for (double v : values) list.add(v); + return list; } } @@ -90,46 +89,18 @@ public void testValuesToObjectArrayWithVector() { assertArrayEquals(new Object[]{1.0, 2.0, 3.0}, result); } - @Test(expected = UnsupportedOperationException.class) + @Test public void testValuesToObjectArrayWithUnsupportedTensor() { Tensor unsupportedTensor = new Tensor() { - @Override - public int size() { - return 1; - } - - @Override - public Object get(int... idx) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Domain getDomain() { - // TODO Auto-generated method stub - return null; - } - - @Override - public int rank() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int[] shape() { - // TODO Auto-generated method stub - return null; - } - - @Override - public boolean isValid(Object value) { - // TODO Auto-generated method stub - return false; - } + @Override public int size() { return 1; } + @Override public Object get(int... idx) { return null; } + @Override public Domain getDomain() { return null; } + @Override public int rank() { return 0; } + @Override public int[] shape() { return null; } + @Override public boolean isValid(Object value) { return false; } }; - - TensorUtils.valuesToObjectArray(unsupportedTensor); + assertThrows(UnsupportedOperationException.class, + () -> TensorUtils.valuesToObjectArray(unsupportedTensor)); } @Test @@ -150,45 +121,17 @@ public void testValuesToDoubleArrayWithVector() { assertArrayEquals(new double[]{1.0, 2.0, 3.0}, result, 1e-10); } - @Test(expected = UnsupportedOperationException.class) + @Test public void testValuesToDoubleArrayWithUnsupportedTensor() { Tensor unsupportedTensor = new Tensor() { - @Override - public int size() { - return 1; - } - - @Override - public Object get(int... idx) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Domain getDomain() { - // TODO Auto-generated method stub - return null; - } - - @Override - public int rank() { - // TODO Auto-generated method stub - return 0; - } - - @Override - public int[] shape() { - // TODO Auto-generated method stub - return null; - } - - @Override - public boolean isValid(Object value) { - // TODO Auto-generated method stub - return false; - } + @Override public int size() { return 1; } + @Override public Object get(int... idx) { return null; } + @Override public Domain getDomain() { return null; } + @Override public int rank() { return 0; } + @Override public int[] shape() { return null; } + @Override public boolean isValid(Object value) { return false; } }; - - TensorUtils.valuesToDoubleArray(unsupportedTensor); + assertThrows(UnsupportedOperationException.class, + () -> TensorUtils.valuesToDoubleArray(unsupportedTensor)); } } \ No newline at end of file diff --git a/beast-base/src/test/java/beast/base/spec/type/TypeUtilsTest.java b/beast-base/src/test/java/beast/base/spec/type/TypeUtilsTest.java index e40b186b..1e2128c0 100644 --- a/beast-base/src/test/java/beast/base/spec/type/TypeUtilsTest.java +++ b/beast-base/src/test/java/beast/base/spec/type/TypeUtilsTest.java @@ -1,10 +1,9 @@ package beast.base.spec.type; -import org.junit.Test; - import beast.base.spec.domain.Domain; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * Test class for TypeUtils @@ -26,26 +25,22 @@ public int[] shape() { @Override public Object get(int... idx) { - // TODO Auto-generated method stub return null; } @Override public Domain getDomain() { - // TODO Auto-generated method stub return null; } @Override public int rank() { - // TODO Auto-generated method stub - return 0; + return shape.length; } @Override public boolean isValid(Object value) { - // TODO Auto-generated method stub - return false; + return true; // test validation in other tests } } diff --git a/beast-base/src/test/java/test/beast/beast2vs1/TestFramework.java b/beast-base/src/test/java/test/beast/beast2vs1/TestFramework.java index b63a7faf..14ab744c 100644 --- a/beast-base/src/test/java/test/beast/beast2vs1/TestFramework.java +++ b/beast-base/src/test/java/test/beast/beast2vs1/TestFramework.java @@ -1,17 +1,23 @@ package test.beast.beast2vs1; -import java.io.File; -import java.util.List; - import beagle.BeagleFlag; import beast.base.inference.Logger; import beast.base.parser.XMLParser; -import beast.base.util.Randomizer; -import static org.junit.jupiter.api.Assertions.assertTrue; - import beast.base.trace.Expectation; import beast.base.trace.LogAnalyser; +import beast.base.util.Randomizer; + +import java.io.File; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * @deprecated Use {@link beast.base.spec.beast2vs1.TestFramework} instead, + * which resolves XML files via {@code getResource()} (classpath-based) + * rather than {@code System.getProperty("user.dir")}. + */ +@Deprecated public abstract class TestFramework { protected static long SEED = 128; private String[] xmls; diff --git a/beast-fx/src/test/java/test/beastfx/integration/ExampleJSONParsingTest.java b/beast-base/src/test/java/test/beast/integration/ExampleJSONParsingTest.java similarity index 85% rename from beast-fx/src/test/java/test/beastfx/integration/ExampleJSONParsingTest.java rename to beast-base/src/test/java/test/beast/integration/ExampleJSONParsingTest.java index 1a6c638d..b9db3661 100644 --- a/beast-fx/src/test/java/test/beastfx/integration/ExampleJSONParsingTest.java +++ b/beast-base/src/test/java/test/beast/integration/ExampleJSONParsingTest.java @@ -1,34 +1,36 @@ -package test.beastfx.integration; +package test.beast.integration; +import beast.base.inference.Logger; +import beast.base.inference.MCMC; +import beast.base.parser.JSONParser; +import beast.base.util.Randomizer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + import java.io.File; import java.io.FilenameFilter; import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.Test; - -import beast.base.inference.Logger; -import beast.base.inference.MCMC; -import beast.base.parser.JSONParser; -import beast.base.util.Randomizer; import static org.junit.jupiter.api.Assertions.assertTrue; /** * check whether all example files parse * */ +// Logger.FILE_MODE and file.name.prefix are JVM-wide globals; serialize all classes +// that mutate them so parallel test runs don't clobber each other's prefix mid-run. +@ResourceLock(value = "beast.logger.globals", mode = ResourceAccessMode.READ_WRITE) public class ExampleJSONParsingTest { - { - ExampleXmlParsingTest.setUpTestDir(); - } - + + @BeforeEach + void setUp() { XMLPathUtil.setUpOutputDir(); } + @Test public void test_ThatXmlExamplesParse() { - String dir = System.getProperty("user.dir") + "/beast.base/examples"; - if (!new File(dir).exists()) { - dir = System.getProperty("user.dir") + "/../beast2/examples"; - } - test_ThatJSONExamplesParse(dir); + test_ThatJSONExamplesParse(XMLPathUtil.resolveExamplesDir()); } public void test_ThatJSONExamplesParse(String dir) { @@ -37,6 +39,7 @@ public void test_ThatJSONExamplesParse(String dir) { Logger.FILE_MODE = Logger.LogFileMode.overwrite; System.out.println("Test JSON Examples in " + dir); File exampleDir = new File(dir); + assertTrue(exampleDir.exists(), "Example directory does not exist: " + dir); String[] exampleFiles = exampleDir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { @@ -73,8 +76,7 @@ public boolean accept(File dir, String name) { @Test public void test_ThatJSONExamplesRun() { - String dir = System.getProperty("user.dir") + "/beast.base/examples"; - test_ThatJSONExamplesRun(dir); + test_ThatJSONExamplesRun(XMLPathUtil.resolveExamplesDir()); } public void test_ThatJSONExamplesRun(String dir) { @@ -82,6 +84,7 @@ public void test_ThatJSONExamplesRun(String dir) { Logger.FILE_MODE = Logger.LogFileMode.overwrite; System.out.println("Test that JSON Examples run in " + dir); File exampleDir = new File(dir); + assertTrue(exampleDir.exists(), "Example directory does not exist: " + dir); String[] exampleFiles = exampleDir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { diff --git a/beast-fx/src/test/java/test/beastfx/integration/ExampleXmlParsingTest.java b/beast-base/src/test/java/test/beast/integration/ExampleXmlParsingTest.java similarity index 90% rename from beast-fx/src/test/java/test/beastfx/integration/ExampleXmlParsingTest.java rename to beast-base/src/test/java/test/beast/integration/ExampleXmlParsingTest.java index fc8b31a0..c807ebfe 100644 --- a/beast-fx/src/test/java/test/beastfx/integration/ExampleXmlParsingTest.java +++ b/beast-base/src/test/java/test/beast/integration/ExampleXmlParsingTest.java @@ -1,6 +1,17 @@ -package test.beastfx.integration; +package test.beast.integration; +import beast.base.inference.Logger; +import beast.base.inference.MCMC; +import beast.base.minimal.BeastMain; +import beast.base.parser.XMLParser; +import beast.base.util.Randomizer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; import java.io.File; import java.io.FilenameFilter; @@ -9,39 +20,21 @@ import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledForJreRange; -import org.junit.jupiter.api.condition.JRE; - -import beastfx.app.beast.BeastMain; -import beast.base.inference.Logger; -import beast.base.inference.MCMC; -import beast.base.parser.XMLParser; -import beast.base.util.Randomizer; import static org.junit.jupiter.api.Assertions.assertTrue; /** * check whether all example files parse * */ +// Logger.FILE_MODE and file.name.prefix are JVM-wide globals; serialize all classes +// that mutate them so parallel test runs don't clobber each other's prefix mid-run. +@ResourceLock(value = "beast.logger.globals", mode = ResourceAccessMode.READ_WRITE) public class ExampleXmlParsingTest { - public static void setUpTestDir() { - // make sure output goes to test directory - File testDir = new File("./test"); - if (!testDir.exists()) { - testDir.mkdir(); - } - System.setProperty("file.name.prefix","test/"); - } - - { - setUpTestDir(); - } + @BeforeEach + void setUp() { XMLPathUtil.setUpOutputDir(); } @Test public void test_ThatXmlExamplesParse() { - // Examples are copied from beast-base test resources to target/test-classes/beast.base/examples/ - String dir = System.getProperty("user.dir") + "/beast.base/examples"; - test_ThatXmlExamplesParse(dir); + test_ThatXmlExamplesParse(XMLPathUtil.resolveExamplesDir()); } public void test_ThatXmlExamplesParse(String dir) { @@ -87,8 +80,7 @@ public boolean accept(File dir, String name) { @Test public void test_ThatXmlExamplesRun() { - String dir = System.getProperty("user.dir") + "/beast.base/examples"; - test_ThatXmlExamplesRun(dir); + test_ThatXmlExamplesRun(XMLPathUtil.resolveExamplesDir()); } public void test_ThatXmlExamplesRun(String dir) { @@ -157,7 +149,7 @@ public ExitException(int status) @DisabledForJreRange(min = JRE.JAVA_18, disabledReason = "SecurityManager removed in Java 18+") @Test public void test_ThatParameterisedXmlExamplesRuns() throws IOException { - String dir = System.getProperty("user.dir") + "/beast.base/examples/parameterised"; + String dir = XMLPathUtil.resolveExamplesDir() + "/parameterised"; Logger.FILE_MODE = Logger.LogFileMode.overwrite; System.out.println("Test that parameterised XML example runs in " + dir + "/RSV2.xml"); Randomizer.setSeed(127); diff --git a/beast-base/src/test/java/test/beast/integration/ResumeTest.java b/beast-base/src/test/java/test/beast/integration/ResumeTest.java new file mode 100644 index 00000000..8cd7bb78 --- /dev/null +++ b/beast-base/src/test/java/test/beast/integration/ResumeTest.java @@ -0,0 +1,158 @@ +package test.beast.integration; + +import beast.base.inference.Logger; +import beast.base.inference.MCMC; +import beast.base.parser.XMLParser; +import beast.base.util.Randomizer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that an MCMC chain can be interrupted and resumed from a state file, + * across a range of model/operator combinations: + * + * testHKY.xml – baseline: tree + continuous params (kappa, base freqs) + * testGTR.xml – constrained rate matrix (DeltaExchangeOperator, sum-to-one params) + * bitflip.xml – discrete BoolVectorParam; no tree, no likelihood + * testOpSubSchedule.xml – custom OperatorSchedule with checkpointed tuning weights + * testPlates.xml – multi-partition model (three data blocks, multiple TreeLikelihoods) + * + * Isolation: each XML gets its own output subdirectory (e.g. test/testHKY/) via + * XMLPathUtil.setUpOutputDir(baseName). testHKY, testOpSubSchedule, and testPlates + * all write to test.$(seed).log / test.$(seed).trees — identical names at seed 127 — + * so subdirectory isolation is essential. Each subdirectory must contain exactly one + * .log file; both the initial run and the resume must produce at least one data sample. + */ +// Logger.FILE_MODE and file.name.prefix are JVM-wide globals; serialize all classes +// that mutate them so parallel test runs don't clobber each other's prefix mid-run. +@ResourceLock(value = "beast.logger.globals", mode = ResourceAccessMode.READ_WRITE) +public class ResumeTest { + private static final long CHAIN_LENGTH = 1000L; + private static final int LOG_EVERY = 100; + + final String[] xmlFiles = { + "testHKY.xml", // tree + continuous params + "testGTR.xml", // constrained rate matrix + "bitflip.xml", // discrete boolean state + "testOpSubSchedule.xml", // custom operator schedule + "testPlates.xml" // multi-partition + }; + + // Remove subdirectories left by previous local runs so the "exactly 1 .log file" + // assertion never sees stale files. CI always starts clean; this only matters locally. + @BeforeEach + void cleanOutputDirs() throws Exception { + for (String xmlFileName : xmlFiles) { + Path dir = Path.of("test/" + xmlFileName.replace(".xml", "")); + if (Files.exists(dir)) { + try (var stream = Files.walk(dir)) { + stream.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); + } + } + } + } + + @Test + public void test_ThatXmlExamplesResume() throws Exception { + Randomizer.setSeed(127); + final String stateFile = "tmp.state"; + + for (String xmlFileName : xmlFiles) { + String baseName = xmlFileName.replace(".xml", ""); + // isolate log/tree output per XML: testHKY, testOpSubSchedule and testPlates + // all resolve to test.127.log / test.127.trees without a unique subdirectory + XMLPathUtil.setUpOutputDir(baseName); + String fileName = new File(XMLPathUtil.resolveExamplesDir(), xmlFileName).getAbsolutePath(); + + // --- initial run --- + System.out.println("Processing " + fileName); + runMcmc(fileName, xmlFileName, stateFile, Logger.LogFileMode.overwrite, false); + System.out.println("Done " + fileName); + + // assert 1: state file must exist and record the correct checkpoint position + // state file XML: + // derive prefix before any run so stateFile and log assertions use the same dir + String prefix = System.getProperty("file.name.prefix"); + File sf = new File(prefix + stateFile); + assertTrue(sf.exists(), sf.getAbsolutePath() + ": state file missing after initial run"); + String stateXml = Files.readString(sf.toPath()); + assertTrue(stateXml.contains("sample='" + CHAIN_LENGTH + "'"), + xmlFileName + ": state file should record sample=" + CHAIN_LENGTH); + + // assert 2: exactly one .log file per output dir — multiple would indicate + // an unexpected logger or a namespace collision between XMLs + File[] logFiles = new File(prefix).listFiles((d, n) -> n.endsWith(".log")); + assertTrue(logFiles != null && logFiles.length == 1, + xmlFileName + ": expected exactly 1 .log file in " + prefix + + ", found " + (logFiles == null ? 0 : logFiles.length)); + Path logFile = logFiles[0].toPath(); + + // assert 3: initial run produced at least one sample + final int beforeResume = assertLogHasSamples(logFile, xmlFileName + " after initial run"); + + // --- resume --- + System.out.println("Resuming " + fileName); + runMcmc(fileName, xmlFileName, stateFile, Logger.LogFileMode.resume, true); + System.out.println("Done " + fileName); + + // assert 4: resumed run appended at least one further sample + final int afterResume = assertLogHasSamples(logFile, xmlFileName + " after resume"); + + assertTrue(afterResume > beforeResume, "Resume should append lines to log in this test : " + + beforeResume + " vs. " + afterResume); + + System.out.println("**********************************\n"); + System.out.println("Complete resume tests for " + fileName); + System.out.println("Initial logging lines " + beforeResume + + ", resume appended " + (afterResume-beforeResume) + " lines."); + System.out.println("**********************************\n\n"); + } + } + + private void runMcmc(String fileName, String xmlFileName, String stateFile, + Logger.LogFileMode fileMode, boolean resuming) throws Exception { + Logger.FILE_MODE = fileMode; + beast.base.inference.Runnable runable = new XMLParser().parseFile(new File(fileName)); + runable.setStateFile(stateFile, resuming); + MCMC mcmc = assertInstanceOf(MCMC.class, runable, + xmlFileName + ": expected an MCMC runnable, cannot test resume"); + mcmc.setInputValue("preBurnin", 0); + mcmc.setInputValue("chainLength", CHAIN_LENGTH); + for (Logger logger : mcmc.loggersInput.get()) { + logger.initByName("logEvery", LOG_EVERY); + } + mcmc.run(); + } + + /** + * Asserts the log file contains a header line starting with "Sample" and + * at least one data line after it. + */ + private static int assertLogHasSamples(Path logFile, String context) throws Exception { + List lines = Files.readAllLines(logFile); + int headerIdx = -1; + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).startsWith("Sample")) { + headerIdx = i; + break; + } + } + assertTrue(headerIdx >= 0, + context + ": log missing header line starting with 'Sample' in " + logFile); + assertTrue(lines.size() > headerIdx + 1, + context + ": log has no data lines after 'Sample' header in " + logFile); + return lines.size(); + } + +} // class ResumeTest diff --git a/beast-base/src/test/java/test/beast/integration/XMLPathUtil.java b/beast-base/src/test/java/test/beast/integration/XMLPathUtil.java new file mode 100644 index 00000000..a0ad58ff --- /dev/null +++ b/beast-base/src/test/java/test/beast/integration/XMLPathUtil.java @@ -0,0 +1,71 @@ +package test.beast.integration; + +import java.io.File; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Shared test infrastructure for beast-base integration tests. + * + *

Two distinct concerns are kept as separate methods on purpose: + *

    + *
  • {@link #resolveExamplesDir()} — pure function; finds where BEAST reads + * XML/JSON input examples from the test classpath.
  • + *
  • {@link #setUpOutputDir()} — side-effectful; creates the {@code ./test/} directory + * and sets {@code file.name.prefix} so BEAST writes log/tree output there. + * Call from {@code @BeforeEach}.
  • + *
+ * Merging them would couple a pure query to a mutating side effect, forcing every + * caller of {@code resolveExamplesDir()} to trigger directory creation implicitly. + * + *

Individual test classes are responsible for naming their own XML/JSON files. + */ +public class XMLPathUtil { + + private static final String EXAMPLES_CLASSPATH = "beast.base/examples"; + + /** + * Returns the absolute path to the beast.base examples directory. + * Resolves via the test classpath (works on any machine or CI runner), + * falling back to {@code user.dir} if the resource is not found. + */ + public static String resolveExamplesDir() { + URL url = XMLPathUtil.class.getClassLoader().getResource(EXAMPLES_CLASSPATH); + if (url != null) { + try { + return new File(url.toURI()).getAbsolutePath(); + } catch (URISyntaxException e) { + // fall through to user.dir fallback + } + } + return System.getProperty("user.dir") + "/" + EXAMPLES_CLASSPATH; + } + + /** + * Creates the {@code ./test/} output directory if absent and sets + * {@code file.name.prefix=test/} so BEAST logger output is written there. + * Call from {@code @BeforeEach} in each integration test class. + */ + public static void setUpOutputDir() { + setUpOutputDir(""); + } + + /** + * Creates {@code ./test//} and sets {@code file.name.prefix} to that + * path, isolating log and tree files for one specific test from those of others. + * Use when multiple tests in the same class share log-file names (e.g. when + * XMLs all write to {@code test.$(seed).log}). + * + *

Note: {@code file.name.prefix} is a JVM-wide system property. Callers that + * mutate it must declare {@code @ResourceLock("beast.logger.globals")} so JUnit 5's + * parallel scheduler serializes them; see {@code junit-platform.properties} for the + * full list of affected classes. + */ + public static void setUpOutputDir(String subdir) { + String path = subdir == null || subdir.isEmpty() ? "test/" : "test/" + subdir + "/"; + File dir = new File("./" + path); + if (!dir.exists()) + dir.mkdirs(); + System.setProperty("file.name.prefix", path); + } +} diff --git a/beast-base/src/test/java/test/beast/integration/XMLProducerTest.java b/beast-base/src/test/java/test/beast/integration/XMLProducerTest.java index a4bbeef0..cd9ea584 100644 --- a/beast-base/src/test/java/test/beast/integration/XMLProducerTest.java +++ b/beast-base/src/test/java/test/beast/integration/XMLProducerTest.java @@ -1,12 +1,5 @@ package test.beast.integration; -import java.io.File; -import java.io.FilenameFilter; -import java.util.ArrayList; -import java.util.List; - -import org.junit.jupiter.api.Test; - import beast.base.core.BEASTInterface; import beast.base.evolution.datatype.Nucleotide; import beast.base.inference.Logger; @@ -14,7 +7,13 @@ import beast.base.parser.XMLParser; import beast.base.parser.XMLProducer; import beast.base.util.Randomizer; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FilenameFilter; +import java.util.ArrayList; +import java.util.List; + import static org.junit.jupiter.api.Assertions.assertTrue; public class XMLProducerTest { @@ -34,9 +33,12 @@ public class XMLProducerTest { @Test public void test_ThatNexusExamplesProduces() { try { - String dirName = System.getProperty("user.dir") + "/beast.base/examples/nexus"; + // Delegate to XMLPathUtil so classpath resolution and user.dir fallback + // logic lives in one place; nexus/ is a subdirectory of examples/. + String dirName = new File(XMLPathUtil.resolveExamplesDir(), "nexus").getAbsolutePath(); System.out.println("Test Nexus Examples in " + dirName); File exampleDir = new File(dirName); + assertTrue(exampleDir.exists(), "Nexus example directory does not exist: " + dirName); String[] exampleFiles = exampleDir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { diff --git a/beast-fx/pom.xml b/beast-fx/pom.xml index 0886aa28..e7b955b3 100644 --- a/beast-fx/pom.xml +++ b/beast-fx/pom.xml @@ -102,10 +102,12 @@ --add-reads beast.fx=ALL-UNNAMED --add-reads beast.base=ALL-UNNAMED --add-reads beast.pkgmgmt=ALL-UNNAMED + --add-exports javafx.base/com.sun.javafx.logging=ALL-UNNAMED --add-exports javafx.graphics/com.sun.glass.ui=ALL-UNNAMED - --add-exports javafx.graphics/com.sun.glass.ui.monocle=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + --add-exports javafx.graphics/com.sun.javafx.util=ALL-UNNAMED --add-opens javafx.graphics/com.sun.glass.ui=ALL-UNNAMED + --add-opens javafx.graphics/com.sun.javafx.application=ALL-UNNAMED glass diff --git a/beast-fx/src/test/java/test/beastfx/integration/ResumeTest.java b/beast-fx/src/test/java/test/beastfx/integration/ResumeTest.java deleted file mode 100644 index 9f16fbee..00000000 --- a/beast-fx/src/test/java/test/beastfx/integration/ResumeTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package test.beastfx.integration; - -import beast.base.inference.Logger; -import beast.base.inference.MCMC; -import beast.base.parser.XMLParser; -import beast.base.util.Randomizer; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.net.URL; - -/** - * check that a chain can be resumed after termination * - */ -public class ResumeTest { - - final static String XML_FILE = "testHKY.xml"; - // Classpath path to the example, copied from beast-base test resources - // by Maven's generate-test-resources phase (see beast-fx/pom.xml). - final static String XML_CLASSPATH = "beast.base/examples/" + XML_FILE; - - { - ExampleXmlParsingTest.setUpTestDir(); - } - - - @Test - public void test_ThatXmlExampleResumes() throws Exception { - Randomizer.setSeed(127); - Logger.FILE_MODE = Logger.LogFileMode.overwrite; - // Resolve via the test classpath so this works on any machine or CI runner - // without relying on user.dir or navigating the source tree. - URL resource = ResumeTest.class.getClassLoader().getResource(XML_CLASSPATH); - if (resource == null) - throw new RuntimeException(XML_CLASSPATH + " not found on test classpath. " + - "Run 'mvn generate-test-resources -pl beast-fx' to copy example files."); - String fileName = new File(resource.toURI()).getAbsolutePath(); - - System.out.println("Processing " + fileName); - XMLParser parser = new XMLParser(); - beast.base.inference.Runnable runable = parser.parseFile(new File(fileName)); - runable.setStateFile("tmp.state", false); - if (runable instanceof MCMC) { - MCMC mcmc = (MCMC) runable; - mcmc.setInputValue("preBurnin", 0); - mcmc.setInputValue("chainLength", 1000l); - mcmc.run(); - } - System.out.println("Done " + fileName); - - System.out.println("Resuming " + fileName); - Logger.FILE_MODE = Logger.LogFileMode.resume; - parser = new XMLParser(); - runable = parser.parseFile(new File(fileName)); - runable.setStateFile("tmp.state", true); - if (runable instanceof MCMC) { - MCMC mcmc = (MCMC) runable; - mcmc.setInputValue("preBurnin", 0); - mcmc.setInputValue("chainLength", 1000l); - mcmc.run(); - } - System.out.println("Done " + fileName); - } // test_ThatXmlExampleResumes - -} // class ResumeTest