Contracts and risk

averageScore divides a candidate's total marks by the number of attempts, and requires that attempts is more than zero. A require that could be false is an uncatchable panic in production - exactly what the report's Contracts section counts. Read scoring.ek9. To play it in the IDE: Help > Lessons > Quality and reports…, or ek9 -ide -lesson contracts-and-risk.

The EK9 IDE as the lesson opens

Start from

The code the lesson starts from: copy it into a file of your own and follow the steps.

domain.ek9

#!ek9
<?-
  Driving Test Centre - a candidate's age.

  REQUIREMENT: a candidate is old enough to book a driving test from age 17 (inclusive) up to 70
  (inclusive) - the Centre does not test anyone outside that range at all.
-?>
defines module quality.centre.domain

  defines constant
    MINIMUM_AGE <- 17

  defines type
    CandidateAge as Integer constrain as
      >= 17 and <= 70

  defines function

    // REQUIREMENT: eligible for a test from MINIMUM_AGE (inclusive) - the Centre's own minimum.
    isMinimumAge()
      -> age as CandidateAge
      <- rtn as Boolean: age > MINIMUM_AGE

//EOF

vehicles.ek9

#!ek9
<?-
  Driving Test Centre - vehicle categories, in their own file (same module as domain.ek9, which a
  module may span) so the Generate Edge-Case Tests… lesson can scope its whole-file run to
  domain.ek9 alone - just CandidateAge and isMinimumAge - without these four trivial classes.
-?>
defines module quality.centre.domain

  defines trait
    Vehicle
      category() as pure
        <- rtn as String?

  defines class
    Car with trait of Vehicle
      override category() as pure
        <- rtn as String: "car"
      default operator ?

    Motorcycle with trait of Vehicle
      override category() as pure
        <- rtn as String: "motorcycle"
      default operator ?

    Lorry with trait of Vehicle
      override category() as pure
        <- rtn as String: "lorry"
      default operator ?

    QuadBike with trait of Vehicle
      override category() as pure
        <- rtn as String: "quad-bike"
      default operator ?

//EOF

scoring.ek9

#!ek9
<?-
  Driving Test Centre - theory test scoring, in its own file (same module as domain.ek9) so the
  Generate Edge-Case Tests… lesson can scope its whole-file run to domain.ek9 alone, without this
  function's require/division in the same scope.
-?>
defines module quality.centre.domain

  defines function

    // REQUIREMENT: a candidate's average theory test score across every attempt made so far -
    // attempts is always at least one (a candidate who has never sat the test has no average yet).
    averageScore()
      ->
        totalMarks as Float
        attempts as Float
      <- rtn as Float?

      require attempts > 0.0
      rtn: totalMarks / attempts

//EOF

service.ek9

#!ek9
<?-
  Driving Test Centre - the booking service: a fee schedule, injected into an application, used by
  a program. Two modules (quality.centre.service depends on quality.centre.domain) for the quality
  report's Module Dependencies page; AbstractFeeSchedule/StandardFeeSchedule for its Type Hierarchy
  and DI Wiring pages.
-?>
defines module quality.centre.service

  references
    quality.centre.domain::CandidateAge

  defines constant
    JUNIOR_AGE <- 25
    SENIOR_AGE <- 60

  defines class

    BookingService
      schedule as AbstractFeeSchedule!

      quote()
        ->
          age as CandidateAge
          category as String
        <- rtn as Money: schedule.feeFor(age, category)

      default operator ?

  defines component

    AbstractFeeSchedule as abstract
      feeFor() as abstract
        ->
          age as CandidateAge
          category as String
        <- rtn as Money?

      default operator ?

    // REQUIREMENT: the test fee depends on the candidate's age band and the vehicle category -
    // three age bands across four categories, the Centre's full published fee schedule.
    StandardFeeSchedule is AbstractFeeSchedule
      override feeFor()
        ->
          age as CandidateAge
          category as String
        <- rtn as Money: 0.00#GBP

        isCar <- category == "car"
        isMotorcycle <- category == "motorcycle"
        isLorry <- category == "lorry"
        isQuadBike <- category == "quad-bike"

        isJunior <- age <= JUNIOR_AGE
        isSenior <- age <= SENIOR_AGE

        if isCar and isJunior
          rtn: 34.00#GBP
        else if isCar and isSenior
          rtn: 40.00#GBP
        else if isCar
          rtn: 38.00#GBP
        else if isMotorcycle and isJunior
          rtn: 28.00#GBP
        else if isMotorcycle and isSenior
          rtn: 32.00#GBP
        else if isMotorcycle
          rtn: 29.00#GBP
        else if isLorry and isJunior
          rtn: 60.00#GBP
        else if isLorry and isSenior
          rtn: 65.00#GBP
        else if isLorry
          rtn: 62.00#GBP
        else if isQuadBike and isJunior
          rtn: 24.00#GBP
        else if isQuadBike and isSenior
          rtn: 27.00#GBP
        else if isQuadBike
          rtn: 26.00#GBP

      default operator ?

  defines application

    CentreApp
      register StandardFeeSchedule() as AbstractFeeSchedule

  defines program

    QuoteFee() with application of CentreApp
      stdout <- Stdout()
      service <- BookingService()
      candidate <- CandidateAge(30)
      fee <- service.quote(candidate, "car")
      stdout.println(`Fee: ${fee}`)

//EOF

dev/tests.ek9

#!ek9
<?-
  Driving Test Centre - a FEW hand-written tests, deliberately leaving real gaps: isMinimumAge is
  only tried at one interior age (never the boundary, where the off-by-one lives), averageScore is
  only tried with ordinary marks (never the require/division edge), and only Car's category() is
  tried at all - Motorcycle, Lorry, QuadBike and the whole fee schedule are untested. Coverage, input
  variety and the budget bars all show what is missing.
-?>
defines module quality.centre.tests

  references
    quality.centre.domain::averageScore
    quality.centre.domain::CandidateAge
    quality.centre.domain::Car
    quality.centre.domain::isMinimumAge

  defines program

    @Test
    AdultIsEligible()
      candidate <- CandidateAge(30)
      assert isMinimumAge(candidate)

    @Test
    CarCategoryIsCar()
      car <- Car()
      assert car.category() == "car"

    @Test
    AverageOfTwoAttempts()
      assert averageScore(150.0, 2.0) == 75.0

//EOF

Step 1: Run the quality report

Run > Quality Report (Shift+Cmd+R) writes the report this lesson reads. Choose Quality Report.

The EK9 IDE once this is done: Run > Quality Report (Shift+Cmd+R) writes the report this lesson reads

Step 2: Risk: hazard times exposure

On index.html, Risk profile: 18 constructs, each scored hazard x exposure (HEX). Hazard is how much a change here can break, from its complexity and coupling. Exposure is how little the tests would notice. The product has two levers, always: make the code less dangerous, or make the tests notice more. The inner ring says how much was measured exactly (6), approximated (2) or has only coverage to go on (10).

The EK9 IDE once this is done: On index.html, Risk profile

Worst measured construct: Lorry.category, score 8, with a link to vehicles.ek9:26. Its hazard is 8% and its exposure 100% - nothing tests it - but it is one line of code, so it is LOW. Worst approximate construct: StandardFeeSchedule.feeFor, 43, MODERATE - hazard 43% (27 paths) and exposure 100%. Its module page shows it as H43 E100 beside the method; isMinimumAge, one test in two, shows H13 E50. "No construct scores HIGH" is a fact about this project, not a promise.

Step 3: Contracts: discharged or unproven

Contracts: 2/4 discharged by construction, 2 unproven. An obligation is a point that could fail for some input - a division by zero, a require that could be false, an unguarded container unwrap, a constrained type built out of range. Discharged means the compiler has already proved it safe for every input. Unproven means nothing has yet - a candidate, not a known failure.

The EK9 IDE once this is done: Contracts

The Contract Verification section names the worst construct: quality.centre.domain::averageScore, scoring.ek9:13, 2 of 2 obligations unproven - its require and its division. On the quality.centre.domain module page it carries C:0/2, amber. An unproven require is exactly where to point the solver.

Step 4: Ask the solver

Choose Code > Generate > Edge-Case Tests (whole file)…

The EK9 IDE once this is done: Choose Code > Generate > Edge-Case Tests (whole file)…

Tick Verify-directed - one @Test per counterexample. It runs the bounded-model solver on every function in scoring.ek9 instead of spraying edge values, so it needs no count or seed.

The EK9 IDE once this is done: Tick Verify-directed - one @Test per counterexample

Choose Generate.

Verified to bound - no counterexamples: the solver found no input within its bound that breaks averageScore, so it wrote no test. Had one existed - a zero that slips past a require, say - it would have been written as a directed @Test, ready to run. Choose Close.

The EK9 IDE just before choosing Close: Verified to bound - no counterexamples

The result

That is risk and contracts. Risk is hazard times exposure: how much a change can break, times how little the tests would notice - so lower either, by simplifying the code or by testing it harder; a low score on untested code means little can go wrong, not that anything is guarded. Contracts are the points that could fail for some input: the compiler discharges those it can prove on every build, and the solver, asked, answers the rest - a concrete failing input to turn into a test, or "verified to bound". Unproven is a question, never a failure. AI assistants ask the same solver with ek9_verify_method, and read both numbers from ek9_run_quality_checks.

The EK9 IDE when the lesson has finished

Where this lesson comes from