The quality report

The Driving Test Centre is a small two-module project: candidates, vehicle categories, a fee schedule injected into an application, and three tests that only try a little of it. Read domain.ek9 - a constrained CandidateAge (17 to 70) and isMinimumAge, which checks it. To play it in the IDE: Help > Lessons > Quality and reports…, or ek9 -ide -lesson quality-report.

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) compiles the project, runs its three tests, and writes the same report ek9 -t6 writes - opened in your own browser when you play this for real; this lesson suppresses that so the IDE keeps the focus. Choose Quality Report.

The EK9 IDE once this is done: Run > Quality Report (Shift+Cmd+R) compiles the project, runs its three tests, and writes the same report ek9 -t6 writes - opened in your own browser when you play this for real; this lesson suppresses that so the IDE keeps the focus

Step 2: The pass/fail pill and Quality at a Glance

On index.html: the header's pill reads "All 3 tests passed" - the three hand-written tests, nothing wrong with them. Quality at a Glance tells a different story: Verification shows three rings - coverage 15.3% (lines that ran), driven 6.6% (input classes actually tried), asserted 6.6% (of those, how many an assertion checked). The CENTRE number is asserted, not coverage: a line running proves nothing if nothing checked what it did.

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

Risk profile: 18 constructs - 6 measured (exact, from a typed input space), 2 approximate (a bare String parameter, like feeFor's category - tighten the type to measure it), 10 coverage-only (no input to vary at all). Contracts: 4 obligations across 3 constructs - 2 discharged by construction (CandidateAge's own bounds), 2 unproven - averageScore's require and division, the worst construct named right there.

The EK9 IDE once this is done: Risk profile

Step 3: Coverage Detail

Coverage Detail: Methods 16.7% (3/18), Lines 15.5% (11/71), Branches 0.0% (0/12) - branches is the hand-written tests' real gap, since none of them chooses between anything. Code Quality Metrics, just above it: Complexity max 27, 18 of 45 headroom - StandardFeeSchedule.feeFor, more than half the hard limit on its own. The next lesson reads that bar on its own.

The EK9 IDE once this is done: Coverage Detail

Step 4: A module, then a file

Follow quality.centre.domain in the Modules table at the foot of index.html: its own page repeats Glance/Metrics/Coverage for just this module, then lists its Source Files - domain.ek9, scoring.ek9, vehicles.ek9 - and every Function & Method, each with its own CC/I/H/C tokens.

The EK9 IDE once this is done: Follow quality.centre.domain in the Modules table at the foot of index.html

Follow domain.ek9 from there: the source itself, highlighted, a check or cross on every line that ran or did not - isMinimumAge's own line is covered (the three hand-written tests call it once), but only ONE of the ages that line could see was ever tried.

Step 5: DI Wiring

The header's three links - DI Wiring, Module Dependencies, Type Hierarchy - are pages of their own. DI Wiring: CentreApp is HEALTHY, registering StandardFeeSchedule as AbstractFeeSchedule; its one injection site, BookingService.schedule, is ticked - every injected field has something registered for it.

The EK9 IDE once this is done: The header's three links - DI Wiring, Module Dependencies, Type Hierarchy - are pages of their own

The result

That is the quality report: the pass/fail pill is about tests, the Verification rings beside it are about proof - asserted is the one that matters, because a line can run (covered) with nothing checking what it did. The module and file pages repeat the same CC/I/H/C tokens the editor's own complexity badges show, down to the construct. AI assistants get the same numbers from ek9_run_quality_checks, without ever opening a page.

Where this lesson comes from