0%

Slotproject: een hulpmiddel voor oefenrapporten · oefening

Beoordeel actualiteit met één meegegeven tijdstip

De cache vermeldt nu wanneer die is opgehaald. Bepalen of dat recent genoeg is, klinkt als een vergelijking van één regel en dat is het bijna. Het interessante is waar de huidige tijd vandaan komt.

De voor de hand liggende versie en waarom die lastig is

Dit is wat de meeste mensen eerst schrijven:

def cache_is_stale(fetched_at, max_age):
    return int(time.time()) - fetched_at > max_age

Het werkt. Probeer het nu te testen.

Je wilt controleren dat een cache die één seconde over zijn limiet is als verouderd geldt. Daarvoor moet time.time() een bepaalde waarde teruggeven. Maar die geeft nu terug, en nu schuift op. Je zou de cache kunnen schrijven met een tijdstempel dat is teruggerekend vanaf de huidige tijd. Dan rekent je test echter om een klok te compenseren, en elke test die iets moet compenseren kan uit de pas gaan lopen.

Je zou de test kunnen laten slapen. Dan is je suite bewust langzamer en nog steeds niet exact.

De functie is niet moeilijk te testen omdat testen moeilijk is. Ze is moeilijk te testen omdat ze zelf een waarde van buitenaf haalt die niemand heeft meegegeven.

Vraag in plaats daarvan om de tijd

Maak de huidige tijd een parameter:

def cache_is_stale(fetched_at, now_epoch, max_age):
    ...

Dat is de hele techniek. Ze heet dependency injection, wat veel grootser klinkt dan het is: geef het benodigde ding mee in plaats van het zelf op te halen.

Nu zegt een test precies wat die bedoelt. De cache is geschreven op 100, het is nu 110 en de limiet is 10 seconden. Is die verouderd? Nee: 110 min 100 is 10 en dat is niet meer dan 10. En op 111? Ja, met één seconde. Beide gevallen worden direct uitgevoerd en geven altijd hetzelfde antwoord.

Waar de klok echt wordt gelezen

Uiteindelijk moet iets time.time() aanroepen. De regel is dat dit één keer gebeurt, hoog in het programma, in de laag die alles aanstuurt:

if now_epoch is None:
    now_epoch = int(time.time())

De aansturende functie leest de klok één keer als niemand een tijd heeft meegegeven en geeft die ene waarde verder door. Tests geven now_epoch mee en de klok wordt nooit aangeraakt.

Eén keer lezen is belangrijker dan het lijkt. Als drie verschillende functies elk time.time() aanroepen, krijgen ze drie iets verschillende antwoorden. Het programma kan dan tijdens dezelfde uitvoering op de ene plek beslissen dat een cache actueel is en op de andere dat die verouderd is. Eén uitlezing, één waarde, doorgegeven naar beneden.

Spreek de grens expliciet uit

“Een uur actueel” heeft een grensgeval en vage code maakt daar stilletjes een keuze over. Zeg het liever expliciet:

  • leeftijd kleiner dan max_age: actueel

  • leeftijd precies gelijk aan max_age: actueel

  • leeftijd één seconde groter dan max_age: verouderd

De vergelijking is dus age > max_age, niet >=. Dat is dezelfde keuze tussen > en >= die je in hoofdstuk 9 debugde. Dat zegt iets over hoe vaak die terugkomt.

Een tijdstempel uit de toekomst is niet actueel

Er blijft één geval over en het verleidelijkste antwoord is verkeerd.

Wat als fetched_at later is dan now_epoch? De leeftijd is negatief. Negatief is zeker kleiner dan max_age, dus een naïeve vergelijking noemt de cache actueel. Een cache met een ongeldig tijdstempel kan dan worden gebruikt totdat dat toekomstige tijdstempel plus max_age is verstreken.

Een tijdstempel uit de toekomst betekent geen actuele gegevens. Het betekent ongeldige gegevens: een klok is veranderd, een bestand is bewerkt, er is iets misgegaan. Werp ValueError op en laat de aanroeper dit behandelen als een ongeldige cache en opnieuw ophalen.

Alle drie de waarden moeten echte niet-negatieve gehele getallen zijn, met de controle type(value) is int uit de vorige les. Voor isinstance is True immers nog steeds een geheel getal.

Nu jij

Implementeer cache_is_stale(fetched_at, now_epoch, max_age) in practice_report.py.

Geef False terug voor actueel, True voor verouderd en werp ValueError op voor elke waarde die geen niet-negatief geheel getal is of wanneer fetched_at in de toekomst ligt.

Roep er geen time.time() in aan. Kijk niet naar wijzigingstijden van bestanden. De drie getallen die je kreeg vormen de volledige invoer.

Waarom krijgt cache_is_stale now_epoch als parameter in plaats van time.time() aan te roepen?

Een cache zegt dat die over 60 seconden is opgehaald. Wat hoort cache_is_stale te doen?

Waarom leest de aansturende laag de klok één keer en geeft die de waarde door?

Opdracht

Voeg de exacte actualiteitsgrens met meegegeven tijd toe en controleer epoch nul, de exacte leeftijdsgrens, één seconde te oud, een tijd in de toekomst en booleaanse tegenvoorbeelden zonder te slapen.