From 1a8b75f96f0c6855581be3a38069f09eca37d1f5 Mon Sep 17 00:00:00 2001 From: Kostya Date: Thu, 10 Sep 2026 11:55:50 +0300 Subject: tests: cover the ex-2 loader, order book trading and desktop state Add tests for the behaviour introduced in the last few commits, so later changes to matching, settlement or the UI state can't quietly break it. On the service side, Ex2LoaderTest loads the ex-2 fixtures and checks that every invalid file is rejected for its own reason, including events with no market maker or two of them, and v1 files. The order book tests cover buys and sells that match or rest in the ladder, self-trade prevention, cash and shares reserved by resting orders, price bounds, risk rejections, and resolution cancelling resting orders and paying the holders. A simulation checks that LMSR settlement conserves money. LedgerMapperTest used to pin the unmapped seq and at fields to their defaults. Now that the mapper fills them from the entry id and time, assert the real values and add a case for a null time. On the desktop side, AppStateTest drives the real ServiceEngine through loading, event creation, LMSR and order book trading, and checks that the observable state updates. Smaller tests check chart timestamps, money and probability formatting, and that every animation settles on its final frame, which is what the UI shows when animations are off. --- .../java/market/guess/ui/desktop/AnimTest.java | 70 +++ .../java/market/guess/ui/desktop/AppStateTest.java | 499 +++++++++++++++++++++ .../java/market/guess/ui/desktop/ChartsTest.java | 32 ++ .../java/market/guess/ui/desktop/FormatTest.java | 29 ++ 4 files changed, 630 insertions(+) create mode 100644 ui-desktop/src/test/java/market/guess/ui/desktop/AnimTest.java create mode 100644 ui-desktop/src/test/java/market/guess/ui/desktop/AppStateTest.java create mode 100644 ui-desktop/src/test/java/market/guess/ui/desktop/ChartsTest.java create mode 100644 ui-desktop/src/test/java/market/guess/ui/desktop/FormatTest.java (limited to 'ui-desktop') diff --git a/ui-desktop/src/test/java/market/guess/ui/desktop/AnimTest.java b/ui-desktop/src/test/java/market/guess/ui/desktop/AnimTest.java new file mode 100644 index 0000000..1a39ba1 --- /dev/null +++ b/ui-desktop/src/test/java/market/guess/ui/desktop/AnimTest.java @@ -0,0 +1,70 @@ +package market.guess.ui.desktop; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import market.guess.ui.desktop.components.graphic.GraphicHelper; +import market.guess.ui.desktop.components.graphic.MarketChartGraphic; +import market.guess.ui.desktop.components.graphic.Motion; +import market.guess.ui.desktop.components.graphic.SparklineGraphic; +import org.junit.jupiter.api.Test; + +/** + * The handoff requires "animations off" to show the frozen final frame, fully legible and never + * blank. Since every graphic renders as a function of progress, that contract is exactly: at + * progress 1, every driver lands on its designed end value. + */ +class AnimTest { + private static final double[] BAR1 = {0, .35, .30, 1, .60, .6, 1, .35}; + + @Test + void keyframesHoldTheirEndValue() { + assertEquals(.35, GraphicHelper.at(0, BAR1), 1e-9); + assertEquals(1.0, GraphicHelper.at(.30, BAR1), 1e-9); + assertEquals(.35, GraphicHelper.at(1, BAR1), 1e-9); + assertEquals(.675, GraphicHelper.at(.15, BAR1), 1e-9); // interpolates linearly between stops + assertEquals(.35, Motion.at(0, BAR1), 1e-9); + } + + @Test + void ridersEndOnTheLastPointOfTheirPath() { + assertEquals( + MarketChartGraphic.YES_CURVE[6], + MarketChartGraphic.rideCurve(MarketChartGraphic.YES_CURVE, 1)[0], + 1e-6); + assertEquals( + MarketChartGraphic.YES_CURVE[7], + MarketChartGraphic.rideCurve(MarketChartGraphic.YES_CURVE, 1)[1], + 1e-6); + + int last = SparklineGraphic.SPARK.length; + assertEquals( + SparklineGraphic.SPARK[last - 2], + SparklineGraphic.ridePolyline(SparklineGraphic.SPARK, 1)[0], + 1e-6); + assertEquals( + SparklineGraphic.SPARK[last - 1], + SparklineGraphic.ridePolyline(SparklineGraphic.SPARK, 1)[1], + 1e-6); + } + + @Test + void ridersStartOnTheFirstPointOfTheirPath() { + assertEquals( + MarketChartGraphic.YES_CURVE[0], + MarketChartGraphic.rideCurve(MarketChartGraphic.YES_CURVE, 0)[0], + 1e-6); + assertEquals( + MarketChartGraphic.YES_CURVE[1], + MarketChartGraphic.rideCurve(MarketChartGraphic.YES_CURVE, 0)[1], + 1e-6); + + assertEquals( + SparklineGraphic.SPARK[0], + SparklineGraphic.ridePolyline(SparklineGraphic.SPARK, 0)[0], + 1e-6); + assertEquals( + SparklineGraphic.SPARK[1], + SparklineGraphic.ridePolyline(SparklineGraphic.SPARK, 0)[1], + 1e-6); + } +} diff --git a/ui-desktop/src/test/java/market/guess/ui/desktop/AppStateTest.java b/ui-desktop/src/test/java/market/guess/ui/desktop/AppStateTest.java new file mode 100644 index 0000000..dbb1762 --- /dev/null +++ b/ui-desktop/src/test/java/market/guess/ui/desktop/AppStateTest.java @@ -0,0 +1,499 @@ +package market.guess.ui.desktop; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicBoolean; +import market.guess.api.CatalogContext; +import market.guess.model.event.CommissionTiming; +import market.guess.model.event.CreateEventRequest; +import market.guess.model.event.MechanismType; +import market.guess.ui.desktop.components.AppTab; +import market.guess.ui.desktop.components.ModalDialog; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AppStateTest { + private AppState state; + private CatalogContext catalog; + private ServiceEngine engine; + + @BeforeEach + void setUp() { + engine = new ServiceEngine(); + var path = Path.of("test-data/ex-2/small.xml"); + var res = engine.getCatalogContext().loadEvents(path); + assertTrue(res.isSuccess(), "Loading small.xml should succeed"); + catalog = engine.getCatalogContext(); + state = new AppState(engine); + state.setLoadedFile(path.toString()); + state.refreshData(); + } + + @Test + void reactivePropertiesNotifyListeners() { + AtomicBoolean skinChanged = new AtomicBoolean(false); + state.skinProperty().addListener((obs, oldVal, newVal) -> skinChanged.set(true)); + state.setSkin(Skin.CATPPUCCIN); + assertTrue(skinChanged.get()); + assertEquals(Skin.CATPPUCCIN, state.getSkin()); + + AtomicBoolean tabChanged = new AtomicBoolean(false); + state.activeTabProperty().addListener((obs, oldVal, newVal) -> tabChanged.set(true)); + state.setActiveTab(AppTab.USERS); + assertTrue(tabChanged.get()); + assertEquals(AppTab.USERS, state.getActiveTab()); + + AtomicBoolean dialogChanged = new AtomicBoolean(false); + state.dialogProperty().addListener((obs, oldVal, newVal) -> dialogChanged.set(true)); + state.setDialog(ModalDialog.CREATE_EVENT); + assertTrue(dialogChanged.get()); + assertEquals(ModalDialog.CREATE_EVENT, state.getDialog()); + } + + @Test + void selectedEventUpdatesWhenIdChanges() { + assertNotNull(state.getSelectedEvent()); + + // Add a new event + var req = + new CreateEventRequest( + "New Test Event", + "Test Description", + MechanismType.LMSR, + 5, + CommissionTiming.ON_PURCHASE, + "Avrum", + 150, + null, + java.util.List.of("YES", "NO"), + true); + var res = catalog.createEvent(req); + assertTrue(res.isSuccess(), "Create event should succeed: " + res.getMessage()); + state.refreshData(); + + String newId = res.getData().summary().key(); + state.setSelectedEventId(newId); + assertEquals(newId, state.getSelectedEvent().id); + assertEquals("New Test Event", state.getSelectedEvent().name); + } + + @Test + void actingUserUpdatesWhenNameChanges() { + assertNotNull(state.getActingUser()); + assertEquals("Avrum", state.getActingUserName()); + + state.setActingUserName("Menash"); + assertNotNull(state.getActingUser()); + assertEquals("Menash", state.getActingUser().name); + } + + @Test + void createEventWithFullParameters() { + var req = + new CreateEventRequest( + "Summit 2026", + "Will the summit take place?", + MechanismType.ORDER_BOOK, + 10, + CommissionTiming.ON_CLOSE, + "Avrum", + null, + new BigDecimal("2.50"), + java.util.List.of("YES", "NO"), + false); + var res = catalog.createEvent(req); + assertTrue(res.isSuccess(), "Create event should succeed: " + res.getMessage()); + var detail = res.getData(); + assertEquals("Summit 2026", detail.summary().name()); + assertEquals(MechanismType.ORDER_BOOK, detail.summary().mechanism()); + assertEquals(10, detail.summary().commissionPercent()); + assertEquals(CommissionTiming.ON_CLOSE, detail.summary().commissionTiming()); + } + + @Test + void testServiceEngineAndXmlLoad() { + assertEquals(2, state.getEvents().size()); + assertEquals(3, state.getUsers().size()); + assertNotNull(state.getActingUser()); + } + + @Test + void testLmsrTradingAndUpdate() { + var openRes = catalog.openEvent("1"); + assertTrue(openRes.isSuccess(), "Opening event 1 should succeed: " + openRes.getMessage()); + state.refreshData(); + + var evBefore = state.event("1"); + assertNotNull(evBefore); + System.out.println( + "Before trade: yesPrice=" + evBefore.yesPrice + ", noPrice=" + evBefore.noPrice); + + var orderRes = + engine.getMarketContext().placeOrder("Avrum", "1", "YES", "BUY", evBefore.yesPrice, 20); + assertTrue(orderRes.isSuccess(), "Place order should succeed: " + orderRes.getMessage()); + + state.refreshData(); + var evAfter = state.event("1"); + assertEquals(new BigDecimal("0.55"), evAfter.yesPrice); + assertEquals(new BigDecimal("0.45"), evAfter.noPrice); + assertEquals(new BigDecimal("0.55"), state.getSelectedEvent().yesPrice); + + System.out.println("=== LMSR EVENT AFTER TRADE ==="); + System.out.println("evAfter.trades.size() = " + evAfter.trades.size()); + for (var t : evAfter.trades) { + System.out.println( + " Trade: n=" + + t.n() + + ", user=" + + t.user() + + ", option=" + + t.option() + + ", yes=" + + t.yesOption() + + ", shares=" + + t.shares() + + ", paid=" + + t.paid()); + } + System.out.println("evAfter.participants.size() = " + evAfter.participants.size()); + for (var p : evAfter.participants) { + System.out.println( + " Participant: user=" + + p.user() + + ", tag=" + + p.tag() + + ", yes=" + + p.yes() + + ", no=" + + p.no() + + ", val=" + + p.value() + + ", fees=" + + p.fees()); + } + var avrum = state.user("Avrum"); + System.out.println("Avrum events size: " + avrum.events.size()); + for (var ue : avrum.events) { + System.out.println( + " userEvent: name=" + + ue.eventName() + + ", role=" + + ue.role() + + ", yes=" + + ue.yes() + + ", no=" + + ue.no()); + } + System.out.println("Avrum balanceHistory size: " + avrum.balanceHistory.size()); + for (var cp : avrum.balanceHistory) { + System.out.println(" BalancePoint: x=" + cp.x() + ", y=" + cp.y()); + } + assertTrue(avrum.balanceHistory.size() >= 2); + assertTrue(avrum.balanceHistory.get(0).x() > 1_000_000_000); + + // Avrum sells 10 YES shares in LMSR + var sellRes = engine.getMarketContext().placeOrder("Avrum", "1", "YES", "SELL", null, 10); + assertTrue( + sellRes.isSuccess(), "Place LMSR sell order should succeed: " + sellRes.getMessage()); + state.refreshData(); + + var evAfterSell = state.event("1"); + var avrumParticipant = + evAfterSell.participants.stream() + .filter(p -> p.user().equals("Avrum")) + .findFirst() + .orElseThrow(); + assertEquals(10, avrumParticipant.yes(), "Avrum should hold 10 YES shares after selling 10"); + assertEquals(10, evAfterSell.yesShares, "Event circulating YES shares should be 10"); + assertEquals(2, evAfterSell.trades.size(), "Event should have 2 trades recorded"); + + var avrumAfterSell = state.user("Avrum"); + var avrumEv = + avrumAfterSell.events.stream() + .filter(e -> e.eventId().equals("1")) + .findFirst() + .orElseThrow(); + assertEquals(10, avrumEv.yes(), "Avrum user event row should show 10 shares"); + } + + @Test + void testOrderBookTradingAndParticipants() { + var openRes = catalog.openEvent("2"); + assertTrue(openRes.isSuccess()); + state.refreshData(); + + var ev2 = state.event("2"); + System.out.println("=== ORDER BOOK EVENT BEFORE TRADE ==="); + System.out.println("ev2.participants.size() = " + ev2.participants.size()); + for (var p : ev2.participants) { + System.out.println( + " Participant: user=" + + p.user() + + ", tag=" + + p.tag() + + ", yes=" + + p.yes() + + ", no=" + + p.no() + + ", val=" + + p.value() + + ", fees=" + + p.fees()); + } + + // Avrum (MM) quotes his minted YES shares; opening the event doesn't do it for him. + var quote = + engine + .getMarketContext() + .placeOrder("Avrum", "2", "YES", "SELL", new BigDecimal("0.50"), 100); + assertTrue(quote.isSuccess(), "Avrum quote should succeed: " + quote.getMessage()); + + // Menash buys 20 YES @ 0.50 on event 2 + var res = + engine + .getMarketContext() + .placeOrder("Menash", "2", "YES", "BUY", new BigDecimal("0.50"), 20); + assertTrue(res.isSuccess(), "Menash place order should succeed: " + res.getMessage()); + state.refreshData(); + + var ev2After = state.event("2"); + System.out.println("=== ORDER BOOK EVENT AFTER TRADE ==="); + System.out.println("ev2After.trades.size() = " + ev2After.trades.size()); + for (var t : ev2After.trades) { + System.out.println( + " Trade: n=" + + t.n() + + ", user=" + + t.user() + + ", option=" + + t.option() + + ", yes=" + + t.yesOption() + + ", shares=" + + t.shares() + + ", paid=" + + t.paid()); + } + System.out.println("ev2After.participants.size() = " + ev2After.participants.size()); + for (var p : ev2After.participants) { + System.out.println( + " Participant: user=" + + p.user() + + ", tag=" + + p.tag() + + ", yes=" + + p.yes() + + ", no=" + + p.no() + + ", val=" + + p.value() + + ", fees=" + + p.fees()); + } + var menash = state.user("Menash"); + System.out.println("Menash events size: " + menash.events.size()); + for (var ue : menash.events) { + System.out.println( + " Menash userEvent: name=" + + ue.eventName() + + ", role=" + + ue.role() + + ", yes=" + + ue.yes() + + ", no=" + + ue.no()); + } + + // Menash SELLS 10 YES @ 0.40 (better price than Avrum's 0.50) + var sellRes = + engine + .getMarketContext() + .placeOrder("Menash", "2", "YES", "SELL", new BigDecimal("0.40"), 10); + assertTrue(sellRes.isSuccess(), "Menash sell order should succeed: " + sellRes.getMessage()); + state.refreshData(); + + // Tikva BUYS 10 YES @ 0.50 (crosses Menash's 0.40 ask!) + var tikvaBuy = + engine + .getMarketContext() + .placeOrder("Tikva", "2", "YES", "BUY", new BigDecimal("0.50"), 10); + assertTrue(tikvaBuy.isSuccess()); + state.refreshData(); + + var ev2AfterSell = state.event("2"); + System.out.println("=== ORDER BOOK EVENT AFTER SELL TRADE ==="); + System.out.println("ev2AfterSell.trades.size() = " + ev2AfterSell.trades.size()); + for (var t : ev2AfterSell.trades) { + System.out.println( + " Trade: n=" + + t.n() + + ", user=" + + t.user() + + ", option=" + + t.option() + + ", yes=" + + t.yesOption() + + ", shares=" + + t.shares() + + ", paid=" + + t.paid()); + } + var domainEv2 = engine.getCatalogContext().getEvent("2"); + System.out.println("Domain event 2 trades count: " + domainEv2.getData()); + long mShares0 = + engine.getCatalogContext().getEvent("2").getData().participants().stream() + .filter(p -> p.user().equals("Menash")) + .findFirst() + .get() + .yes(); + System.out.println("Menash YES shares in ParticipantDTO: " + mShares0); + System.out.println("ev2AfterSell.participants.size() = " + ev2AfterSell.participants.size()); + for (var p : ev2AfterSell.participants) { + System.out.println( + " Participant: user=" + + p.user() + + ", tag=" + + p.tag() + + ", yes=" + + p.yes() + + ", no=" + + p.no() + + ", val=" + + p.value() + + ", fees=" + + p.fees()); + } + menash = state.user("Menash"); + for (var ue : menash.events) { + System.out.println( + " Menash userEvent after sell: name=" + + ue.eventName() + + ", role=" + + ue.role() + + ", yes=" + + ue.yes() + + ", no=" + + ue.no()); + } + + var menashP = + ev2AfterSell.participants.stream() + .filter(p -> p.user().equals("Menash")) + .findFirst() + .orElseThrow(); + assertEquals(10, menashP.yes(), "Menash should have 10 YES shares after selling 10 of 20"); + var tikvaP = + ev2AfterSell.participants.stream() + .filter(p -> p.user().equals("Tikva")) + .findFirst() + .orElseThrow(); + assertEquals(10, tikvaP.yes(), "Tikva should have 10 YES shares after buying 10"); + var avrumP = + ev2AfterSell.participants.stream() + .filter(p -> p.user().equals("Avrum")) + .findFirst() + .orElseThrow(); + assertEquals(80, avrumP.yes(), "Avrum (MM) should have 80 YES shares"); + assertEquals(2, ev2AfterSell.trades.size(), "Order book event should have 2 trades"); + } + + @Test + void testEventsTabControllerProgressBar() throws Exception { + try { + javafx.application.Platform.startup(() -> {}); + } catch (IllegalStateException ignored) { + } + + var loader = new javafx.fxml.FXMLLoader(getClass().getResource("controllers/events_tab.fxml")); + javafx.scene.Parent root = loader.load(); + market.guess.ui.desktop.controllers.EventsTabController ctrl = loader.getController(); + + ctrl.init(state); + ctrl.refresh(); + + var field = + market.guess.ui.desktop.controllers.EventsTabController.class.getDeclaredField( + "lmsrYesProgressBar"); + field.setAccessible(true); + javafx.scene.control.ProgressBar pb = (javafx.scene.control.ProgressBar) field.get(ctrl); + + assertEquals(0.5, pb.getProgress(), 0.001); + + // Switch to event 2 (Order Book) + state.setSelectedEventId("2"); + // Switch back to event 1 (LMSR) + state.setSelectedEventId("1"); + assertEquals(0.5, pb.getProgress(), 0.001); + + // Open event 1 + catalog.openEvent("1"); + state.refreshData(); + assertEquals(0.5, pb.getProgress(), 0.001); + + // Trade 20 YES on event 1 + var res = + engine + .getMarketContext() + .placeOrder("Avrum", "1", "YES", "BUY", state.event("1").yesPrice, 20); + assertTrue(res.isSuccess()); + state.refreshData(); + assertEquals(0.55, pb.getProgress(), 0.001); + + root.getStyleClass().add("gm-root"); + javafx.scene.Scene scene = new javafx.scene.Scene(root); + scene.getStylesheets().add(getClass().getResource("theme.css").toExternalForm()); + root.applyCss(); + root.layout(); + + javafx.scene.Node track = pb.lookup(".track"); + javafx.scene.Node bar = pb.lookup(".bar"); + assertNotNull(track); + assertNotNull(bar); + if (bar instanceof javafx.scene.layout.Region rBar) { + assertEquals(0.0, rBar.getPadding().getTop(), 0.001); + assertEquals(0.0, rBar.getPadding().getBottom(), 0.001); + assertNotNull(rBar.getBackground()); + assertEquals(1, rBar.getBackground().getFills().size()); + assertEquals(0.0, rBar.getBackground().getFills().get(0).getInsets().getTop(), 0.001); + } + if (track instanceof javafx.scene.layout.Region rTrack) { + assertEquals(0.0, rTrack.getPadding().getTop(), 0.001); + assertEquals(0.0, rTrack.getPadding().getBottom(), 0.001); + assertNotNull(rTrack.getBackground()); + } + } + + @Test + void testEventDataAndUserDataEquals() { + var ev1 = state.event("1"); + assertNotNull(ev1); + var ev1Copy = + new market.guess.ui.desktop.model.EventData( + ev1.id, + ev1.num, + ev1.name, + ev1.desc, + ev1.type, + ev1.status, + ev1.mm, + ev1.feePercent, + ev1.feeMode, + ev1.contract.doubleValue(), + ev1.liquidityB, + ev1.baseValueD); + assertEquals(ev1, ev1Copy); + assertEquals(ev1.hashCode(), ev1Copy.hashCode()); + + var u1 = state.user("Avrum"); + assertNotNull(u1); + var u1Copy = + new market.guess.ui.desktop.model.UserData( + "Avrum", u1.role, u1.balance.doubleValue(), u1.blocked, u1.isMm); + assertEquals(u1, u1Copy); + assertEquals(u1.hashCode(), u1Copy.hashCode()); + } +} diff --git a/ui-desktop/src/test/java/market/guess/ui/desktop/ChartsTest.java b/ui-desktop/src/test/java/market/guess/ui/desktop/ChartsTest.java new file mode 100644 index 0000000..c5cb280 --- /dev/null +++ b/ui-desktop/src/test/java/market/guess/ui/desktop/ChartsTest.java @@ -0,0 +1,32 @@ +package market.guess.ui.desktop; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import market.guess.ui.desktop.util.Charts; +import org.junit.jupiter.api.Test; + +class ChartsTest { + + @Test + void testToEpochSecondFromIndex() { + double t0 = Charts.toEpochSecond(0); + double t1 = Charts.toEpochSecond(1); + double t2 = Charts.toEpochSecond(2); + + assertEquals(Charts.BASE_TIME, t0, 1e-6); + assertEquals(Charts.BASE_TIME + 1800, t1, 1e-6); + assertEquals(Charts.BASE_TIME + 3600, t2, 1e-6); + } + + @Test + void testToEpochSecondIdempotentForTimestamps() { + long epoch = 1_700_000_000L; + assertEquals(epoch, Charts.toEpochSecond(epoch), 1e-6); + } + + @Test + void testBaseTimeIsReasonable() { + assertTrue(Charts.BASE_TIME > 1_700_000_000L); + } +} diff --git a/ui-desktop/src/test/java/market/guess/ui/desktop/FormatTest.java b/ui-desktop/src/test/java/market/guess/ui/desktop/FormatTest.java new file mode 100644 index 0000000..956a322 --- /dev/null +++ b/ui-desktop/src/test/java/market/guess/ui/desktop/FormatTest.java @@ -0,0 +1,29 @@ +package market.guess.ui.desktop; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigDecimal; +import market.guess.ui.desktop.util.Format; +import org.junit.jupiter.api.Test; + +class FormatTest { + + @Test + void testMoneyFormatting() { + assertEquals("$0.00", Format.money((BigDecimal) null)); + assertEquals("$0.00", Format.money(BigDecimal.ZERO)); + assertEquals("$0.50", Format.money(new BigDecimal("0.50"))); + assertEquals("$123.45", Format.money(new BigDecimal("123.45"))); + assertEquals("-$5.00", Format.money(new BigDecimal("-5.00"))); + assertEquals("$100.00", Format.money(100.0)); + assertEquals("-$2.50", Format.money(-2.50)); + } + + @Test + void testProbFormatting() { + assertEquals("0.50", Format.prob(0.50)); + assertEquals("0.75", Format.prob(0.751)); + assertEquals("0.00", Format.prob(0.00)); + assertEquals("1.00", Format.prob(1.00)); + } +} -- cgit v1.2.3