diff --git a/oppgavetekster/oving6/HighscoreList.md b/oppgavetekster/oving6/HighscoreList.md new file mode 100644 index 0000000..d0afd31 --- /dev/null +++ b/oppgavetekster/oving6/HighscoreList.md @@ -0,0 +1,46 @@ +# Observatør-observert-teknikken - HighscoreList-oppgave + +Denne oppgaven handler om å bruke observatør-observert-teknikken for å bli informert om endringer i en highscore-liste. + +Observatør-observert-teknikken går ut på at det observerte objektet sier ifra til en eller flere observatører om at tilstanden er endret. Dette brukes gjerne når vi har en rekke observatørobjekter som ønsker å vite når en endring skjer i et annet (observert) objekt. Det hadde vært ueffektivt om observatørobjektene skulle sjekket for endringer hele tiden. Derfor definerer vi ofte et felles `interface` som disse kan implementere, slik at det observerte objektet kan kalle på metoder i observatørene når det skjer en endring. + +I denne oppgaven skal vi lage en observerbar `HighscoreList` som kan si fra til observatører/lyttere av typen `HighscoreListListener` når nye resultater blir registrert. En hovedprogramklasse kalt `HighscoreProgram` vil bli brukt til å sjekke at det virker. Denne klassen oppretter en `HighscoreList`-instans, legger inn resultater (tall) fra konsollen som legges til listen og skriver ut listen hver gang et nytt resultat faktisk blir lagt til. + +Alle filene i denne oppgaven skal lages i [oving6/highscorelist](../../src/main/java/oving6/highscorelist). + +## Del 1: Implementasjon av HighscoreList + +En `HighscoreList` skal holde styr på heltallsresultater (av typen `int`/`Integer`). Listen skal være observerbar ved at den kan registrere lyttere (`HighscoreListListener`-instanser) og si fra til dem når listen blir endret. Listen skal ha en maksimal lengde, som settes i konstruktøren, f.eks. skal en topp 10-liste kunne opprettes med `new HighscoreList(10)`. Nye resultater registreres med metoden `addResult(int)`, som skal finne riktig posisjon og legge resultatet inn (dersom det er godt nok). Dersom listen er for lang, så skal det dårligste resultatet fjernes. NB: _Lavest verdi_ er best, f.eks. antall sekunder på en oppgave eller antall flytt i Sokoban. + +`HighscoreListListener`-grensesnittet er vist i klassediagrammet til venstre og må implementeres av alle klasser som ønsker å fungere som lyttere for `HighscoreList`-instanser. Lyttere registrerer seg med `HighscoreList` sin `addHighscoreListListener`-metode og vil siden få beskjed om nye resultater ved at `listChanged()`-metoden kalles. Argumentene som tas inn er `HighscoreList`-objektet som ble endret og posisjonen i listen der endringen skjedde. + +Her er en oversikt over metoden som må implementeres: + +- `HighscoreList(int maxSize)` - konstruktøren tar inn maks antall resultater som listen skal kunne holde. Denne verdien må brukes av `addResult`, slik at resultater som er for dårlige kastes. +- `int size()` - returnerer antall elementer i listen, som altså aldri skal overstige maks-antallet. +- `int getElement(int)` - returnerer resultatet i posisjonen angitt av argumentet. +- `void addResult(int)` - registrere et nytt resultat, og dersom resultatet er godt nok til å komme med på listen, så legges det inn på riktig plass. Dersom listen blir for lang, så må dårligste resultat kastes. Alle registrerte lyttere må få beskjed om en evt. endring av listen, inkludert på hvilken posisjon som ble endret. +- `void addHighscoreListListener(HighscoreListListener)` - registrerer en ny lytter. +- `void removeHighscoreListListener(HighscoreListListener)` - fjerner en tidligere registrert lytter. + +Klassediagram for `HighscoreList` og `HighscoreListListener`: + +![highscore-list](./img/highscore-list.png) + +Testkode for denne oppgaven finner du her: [oving6/highscorelist/HighscoreListTest.java](../../src/test/java/oving6/highscorelist/HighscoreListTest.java). + +## Del 2: Hovedprogramklasse + +Lag en hovedprogramklasse kalt `HighscoreListProgram`, som tester at `HighscoreList`-klassen din virker som den skal. La den opprette en `HighscoreList`-instans, lese inn tall fra konsollet (f.eks. med en `Scanner` og `nextInt`-metoden) og legge disse inn i listen. Sørg for at `HighscoreListProgram` implementerer `HighscoreListListener`-grensesnittet (`HighscoreListProgram implements HighscoreListListener`) og registrerer seg som lytter på `HighscoreList`-instansen via `addHighscoreListListener`. La lyttermetoden `listChanged` skrive ut informasjon og resultatene i `HighscoreList`-instansen og posisjonsargumentet, slik at du ser at alt virker som det skal. + +Vi foreslår følgende metoder og oppførsel: + +- `void init()` - oppretter en ny `HighscoreList` og registrerer seg selv (altså `HighscoreListProgram`-instansen) som lytter. Dette kan og gjøres i konstruktøren om ønskelig. +- `void run()` - leser inn tall (resultater) fra terminalen og legger dem til i listen. +- `void listChanged(HighscoreList, int)` (fra `HighscoreListListener`) - observerer endringer i `HighscoreList`-instansen og skriver ut posisjonsargumentet, samt selve listen, til konsollen. + +Klassediagrammet viser hvordan klassene henger sammen, og vårt forslag til metoder: + +![hl-program](./img/hl-program.png) + +Husk også å lage en `main()`-metode som kjører HighscoreListProgram! diff --git a/oppgavetekster/oving6/Logger.md b/oppgavetekster/oving6/Logger.md new file mode 100644 index 0000000..60e9222 --- /dev/null +++ b/oppgavetekster/oving6/Logger.md @@ -0,0 +1,136 @@ +# Delegering - Logger-oppgave + +Denne oppgaven bruker delegeringsteknikken for å implementere en fleksibel måte å håndtere logging (av feil i programmer) på. + +## Logging + +Ved kjøring av programmer er det ofte behov for å logge hva som skjer underveis, slik at det blir lettere å drive feilsøking i etterkant. F.eks. kan en lagre feilmeldinger til fil, med tidspunkt og litt om programtilstanden og hvis programmet kræsjer ordentlig, så kan brukeren sende logg-fila som e-post til utviklerne. En enkel måte å støtte logging på er å lage en hjelpeklasse med én metode, f.eks. `log(String melding)`, og så er det hjelpeklassen som bestemmer om meldingen skal vises i statuslinja, skrives til fil, sendes som melding til en alarmsentral osv. Hjelpeklassen kan kanskje brukes av mange programmer, og siden behovene vil variere er det viktig å gjøre dette fleksibelt. Denne oppgaven bruker [grensesnitt](https://www.ntnu.no/wiki/pages/viewpage.action?pageId=65936813) og [delegeringsteknikken](https://www.ntnu.no/wiki/display/tdt4100/Delegeringsteknikken) for å implementere fleksibel logging, litt på samme måte som eksisterende loggingsrammeverk (se f.eks. [Java sin egen loggingsfunksjonalitet](http://docs.oracle.com/javase/6/docs/technotes/guides/logging/overview.html), Apache sitt [log4j-rammeverk](http://logging.apache.org/log4j/), eller Googles ["Java logging framework"](https://www.google.no/search?q=java+logging+frameworks)). + +Alle filene i denne oppgaven skal lages i [oving6/logger](../../src/main/java/oving6/logger). + +### ILogger-grensesnittet + +Logging gjøres ved å bruke ulike implementasjoner av `ILogger`, som er definert som følger: + +```java +package oving6.logger; + +public interface ILogger { + + String ERROR = "error"; + String INFO = "info"; + String WARNING = "warning"; + + void log(String severity, String message, Exception exception); +} +``` + +ILogger-grensesnittet definerer én `log`-metode som brukes til all logging: + +- `severity`-argumentet angir alvorlighetsgraden, og må være en av `String`-verdiene `ERROR`, `WARNING` eller `INFO`, som er definert som konstanter i grensesnittet. +- `message`-argumentet er en melding om hva som var feil. +- `exception`-argumentet er et unntaksobjekt, som kan gi mer informasjon av hva som var feil, men kan være `null`. + +En typisk bruk vil være i `catch`-delen av en `try/catch`: + +```java +ILogger logger = ... +... +try { + ... +} catch (IOException ioe) { + logger.log(ILogger.ERROR, "Feil ved lesing fra fil", ioe); +} +``` + +Akkurat hvordan logging utføres bestemmes av hvilken implementasjon av ILogger-grensesnittet en bruker, og i denne oppgaven skal du implementere følgende tre klasser: + +- `DistributingLogger` - delegerer til andre loggere basert på _alvorlighetsgraden_. +- `FilteringLogger` - delegerer til en annen logger, men kun for spesifikke alvorlighetsgrader. +- `StreamLogger` - skriver logg-meldingen til en angitt strøm. + +Hver av disse utgjør én av deloppgavene beskrevet under. + +## Del 1 - StreamLogger + +En `StreamLogger` sørger for å skrive alle logg-meldinger til en angitt `OutputStream`, med én melding pr. linje (altså linjeskift mellom hver melding). `OutputStream`-objektet må gis inn i konstruktøren: + +- `StreamLogger(OutputStream stream)` - initialiserer `StreamLogger`-objektet slik at logg-meldinger skrives til `stream`. + +Eksempel på bruk: + +```java +ILogger logger = new StreamLogger(System.out); +logger.log(ILogger.INFO, "Denne meldingen er til informasjon og skrives til System.out", null); +``` + +Husk å kalle `flush`-metoden til OutputStream etter at logg-meldingen er skrevet. + +Det skal også være mulig å angi en såkalt _format_-string, dvs. en `String` som fungerer som en slags mal for hva som skrives, f.eks. `"%s: %s (%s)"`: + +- `void setFormatString(String formatString)` - setter format-string-en som brukes for å lage logg-meldingen som skrives. Det settes ingen andre krav til validering av `formatString`-argumentet annet enn at det ikke kan være `null`. + +Effekten av skriving skal være som om man ga format-string-en som første argument til `String.format`-metoden etterfulgt av `severity`-, `message`- og `exception`-argumentene, og så skrev ut det denne metoden returnerer: + +```java +String logMessage = String.format(formatString, severity, message, exception); +// skriv logMessage til OutputStream-en her +``` + +Merk at dersom format-string-en ikke er satt, så skal den ha en fornuftig start-verdi. + +Testkode for oppgaven: [oving6/logger/StreamLoggerTest.java](../../src/test/java/oving6/logger/StreamLoggerTest.java). + +## Del 2 - FilteringLogger + +`FilteringLogger`-klassen implementerer `ILogger`-grensesnittet og delegerer til en annen `ILogger`-implementasjon, men bare hvis _alvorlighetsgraden_ er en av et sett angitte verdier. Både loggeren det delegeres til og alvorlighetsgradene angis når `FilteringLogger`-objektet opprettes: + +- `FilteringLogger(ILogger logger, String... severities)` - initialiserer `FilteringLogger`-objektet så det delegerer logging til `logger`-argumentet, men bare hvis _alvorlighetsgraden_ som gis til `log`-metoden er en av verdiene angitt i `severities`-argumentet. `severities`-argumentet er et såkalt varargs-argument, som du kan lese mer om her: [Varargs - variabelt antall argumenter](https://www.ntnu.no/wiki/display/tdt4100/Varargs+-+variabelt+antall+argumenter). Det viktigste å vite her er at det du får inn i metoden din vil være en variabel `severities` som er av typen string array (`String[]`). Du kan hente ut elementer her via `severities[0]`, sjekke lengde ved `severities.length` og ellers bruke alle normale arraymetoder. + +Det skal også være mulig å sjekke om logging er på og slå logging av og på i etterkant: + +- `boolean isLogging(String severity)` - returnerer `true` hvis logging er slått på for den angitte alvorlighetsgraden og `false` ellers. +- `void setIsLogging(String severity, boolean value)` - slår logging på (`value` er `true`) eller av (`value` er `false`) for den angitte _alvorlighetsgraden_. + +Eksempel på bruk: + +```java +ILogger syserrLogger = new StreamLogger(System.err); +FilteringLogger logger = new FilteringLogger(syserrLogger, ILogger.ERROR); + +logger.log(ILogger.ERROR, "Denne meldingen er alvorlig og skrives til System.err", null); +logger.log(ILogger.WARNING, "Denne meldingen er en advarsel og blir filtrert bort", null); +logger.log(ILogger.INFO, "Denne meldingen er til informasjon og blir filtrert bort", null); + +logger.setIsLogging(ILogger.WARNING, true); +logger.log(ILogger.WARNING, "Denne meldingen er en advarsel og blir nå skrevet til System.err", null); +``` + +Testkode for oppgaven: [oving6/logger/FilteringLoggerTest.java](../../src/test/java/oving6/logger/FilteringLoggerTest.java). + +## Del 3 - DistributingLogger + +`DistributingLogger`-klassen brukes for å fordele logg-meldinger til en av tre andre loggere, avhengig av _alvorlighetsgraden_ til en logg-melding. Den har én hjelpe-logger for meldinger med alvorlighetsgrad `ERROR`, én for meldinger av alvorlighetsgrad `WARNING` og en for meldinger av alvorlighetsgrad `INFO`. Alle disse angis til konstruktøren: + +- `DistributingLogger(ILogger errorLogger, ILogger warningLogger, ILogger infoLogger)` - initialiserer objektet slik at den første loggeren brukes til alvorlighetsgraden `ERROR`, den andre til alvorlighetsgraden `WARNING` og den tredje til alvorlighetsgraden `INFO`. + +I tillegg skal klassen ha en metode for å sette hver av dem individuelt: + +- `void setLogger(String severity, ILogger logger)` - setter/endrer loggeren som brukes for den angitte alvorlighetsgraden. + +Eksempel på bruk: + +```java +ILogger syserrLogger = new StreamLogger(System.err); +ILogger sysoutLogger = new StreamLogger(System.out); +DistributingLogger logger = new DistributingLogger(syserrLogger, syserrLogger, sysoutLogger); + +logger.log(ILogger.ERROR, "Denne meldingen er alvorlig og skrives til System.err", null); +logger.log(ILogger.WARNING, "Denne meldingen er en advarsel og skrives til System.err", null); +logger.log(ILogger.INFO, "Denne meldingen er til informasjon og skrives til System.out", null); + +logger.setLogger(ILogger.WARNING, sysoutLogger); +logger.log(ILogger.WARNING, "Denne meldingen er en advarsel, men nå skrives den til System.out", null); +``` + +Testkode for oppgaven: [oving6/logger/DistributingLoggerTest.java](../../src/test/java/oving6/logger/DistributingLoggerTest.java). diff --git a/oppgavetekster/oving6/Office.md b/oppgavetekster/oving6/Office.md new file mode 100644 index 0000000..a05629b --- /dev/null +++ b/oppgavetekster/oving6/Office.md @@ -0,0 +1,54 @@ +# Delegering - The Office-oppgave + +Denne oppgaven bruker delegeringsteknikken for å modellere arbeidsfordeling på en “vanlig” arbeidsplass. Denne oppgaven kan muligens oppleves som mindre meningsfull. Dette er kanskje omtrent tilsvarende hvor meningsløst noen typer kontorarbeid kan virke. + +Vi skal i dette scenarioet ha en sjef, eller `Manager`, som har én eller flere arbeidere, eller `Clerk`s, altså i en såkalt én-til-mange relasjon. Et `Employee`-grensesnitt definerer en oppførsel som er felles for de ansatte, og implementeres av både `Manager` og `Clerk`. + +`Employee`-objekter på denne simulerte arbeidsplassen har to oppgaver: + +- Utskrift av dokumenter +- Utførelse av matematiske beregninger + +Alle filene i denne oppgaven skal lages i [oving6/office](../../src/main/java/oving6/office). + +## Del 1: Employee, Clerk og Printer + +`Employee`-grensesnittet har følgende metoder: + +- `double doCalculations(BinaryOperator operation, double value1, double value2)` - regner ut resultatet av å utføre `operation` med argumentene `value1` og `value2`. +- `void printDocument(String document)` - Printer `document`. Hvordan dette gjøres avhenger av den spesifikke implementasjonen. +- `int getTaskCount()` - returnerer hvor mange oppgaver (beregninger og printinger) som har blitt utført av eller på vegne av dette `Employee`-objektet. +- `int getResourceCount()` - antallet employees til rådighet, inkludert `Employee`-objektet metoden blir kalt på. En `Employee` skal altså medregne seg selv i antall ressurser den ansatte har til rådighet. Dette tallet skal inkludere alle `Employee`-objekter nedover i hierarkiet. + +Lag dette grensesnittet, og lag så en `Clerk`-klasse som implementerer det. `Clerk` må ha følgende konstruktør: + +- `Clerk(Printer printer)` + +`Clerk`-klassen må inneholde egen logikk for å løse `doCalculations`, men skal altså delegere `printDocuments` til `Printer`-objektet gitt i konstruktøren. + +Definer en `Printer`-klasse med følgende metoder: + +- `void printDocument(String document, Employee employee)` - skriver documentet til konsollen og tar vare på dokumentet i `employee` sin historikk. Ingen av argumentene kan være `null`. +- `List getPrintHistory(Employee employee)` - returnerer en `List` med alle dokumentene som har blitt printet av `employee` av denne printeren i rekkefølgen de har blitt printet. Om `employee` ikke har printet noen dokumenter ved denne printeren skal en tom liste returneres. + +La så `Clerk` delegere `printDocument` til `Printer`. Siden `Clerk` ikke har noen andre ansatte å delegere til, vil `getResourceCount()` alltid være 1. + +Testkode for `Clerk` er her: [oving6/office/ClerkTest.java](../../src/test/java/oving6/office/ClerkTest.java). + +Testkode for `Printer` er her: [oving6/office/PrinterTest.java](../../src/test/java/oving6/office/PrinterTest.java). + +## Del 2: Manager + +Vi definerer så sjefen til de hardt-arbeidende `Clerk`-objektene. `Manager`-klassen har følgende konstruktør: + +- `Manager (Collection employees)` - utløser et `IllegalArgumentException` dersom employees er tom. + +La så `Manager` implementere `Employee`-grensesnittet. Implementer `Manager`s oppgaver ved å delegere alle videre til en av arbeiderne i listen med `Employee`-objekter gitt i konstruktøren. Regelen for hvilken `Employee` som får hvilken oppgave delegert til seg kan du bestemme selv, men prøv å gjøre det slik at arbeidet fordeles jevnt på alle. Mens `Clerk` altså har kun én tilgjengelig ressurs vil `Manager`-objekter vil ha flere. + +Testkode for `Manager` er her: [oving6/office/ManagerTest.java](../../src/test/java/oving6/office/ManagerTest.java). + +## Del 3: Main-metode + +Lag en `main()`-metode som illustrerer hva som skjer med effektiviteten når vi legger til flere nivåer med mellomledere. + +Lag først et `Manager`-objekt som blir tildelt noen `Clerk`-objekter under seg. Presentér deretter effektiviteten av hierarkiet ved å skrive ut `getTaskCount() / getResourceCount()` for `Manager`-objektet. Vis deretter hvordan effektiviteten faller når vi legger til nivåer med mellomledere ved å lage to eller flere nivåer med `Manager`, hvor lederne på bunnen tildeles `Clerk`-objekter, og skriv ut den nye effektiviteten for topplederne. diff --git a/oppgavetekster/oving6/README.md b/oppgavetekster/oving6/README.md new file mode 100644 index 0000000..58b7a91 --- /dev/null +++ b/oppgavetekster/oving6/README.md @@ -0,0 +1,44 @@ +# Øving 06: Observatør-Observert og Delegering + +## Øvingsmål + +- Lære hva observatør-observert-teknikken er, dens bruksområder og fordeler +- Lære bruk av delegering for å utføre oppgaver i en klasse + +## Øvingskrav + +- Kunne definere og implementere et observatørgrensesnitt +- Kunne la en observert klasse fortelle dens observatører om endringer +- Kunne la en klasse delegere utførelsen av oppgaver til interne objekter + +## Dette må du gjøre + +### Del 1: Programmering + +Denne øvingen omfatter både [delegeringsteknikken](https://www.ntnu.no/wiki/display/tdt4100/Delegeringsteknikken) og [observatør-observert-teknikken](https://www.ntnu.no/wiki/pages/viewpage.action?pageId=66879660). Gjør **minst én** av de fire oppgavene under. For å få 2 poeng må det gjøres **minst én** oppgave fra **hvert av de to temaene**. Dette anbefales uansett på det *sterkeste*, siden dette må til for å dekke hele pensum. + +Gjennomfør enten *minst én* av oppgavene om delegering: + +- [The Office](./Office.md) (anbefalt) (Lett) +- [Logger](./Logger.md) (Medium) + +ELLER *minst én* av oppgavene om observatør-observert-teknikken: + +- [StockListener](./StockListener.md) (Medium) +- [Highscore](./HighscoreList.md) (Vanskelig) + +**I tillegg til oppgaven(e) ovenfor skal du levere en tekstfil hvor du gjør kort rede for delegeringsteknikken og observatør-observert-teknikken.** + +### Del 2: Objektdiagram + +For en av oppgavene du gjorde i del 1: + +Lag en sekvens av kall i `main()`-metoden. Denne sekvensen må benytte seg av den passende teknikken fra del 1. Tegn deretter et [objektdiagram](https://www.ntnu.no/wiki/display/tdt4100/Objektdiagrammer) som viser tilstanden til hvert objekt ved slutten av `main()`-metoden. Du trenger ikke levere inn diagrammet på Blackboard. + +## Hjelp / mistanke om bugs + +Ved spørsmål eller behov for hjelp konsulter studassen din i saltidene hans/hennes. Du kan også oppsøke andre studasser på sal eller legge ut et innlegg på [Piazza](https://piazza.com/ntnu.no/spring2025/tdt4100). + +## Godkjenning + +Last opp kildekode på Blackboard innen den angitte innleveringsfristen. Innlevert kode skal demonstreres for en læringsassistent innen én uke etter innleveringsfrist. Se for øvrig Blackboard-sidene for informasjon rundt organisering av øvingsopplegget og det tilhørende øvingsreglementet. diff --git a/oppgavetekster/oving6/StockListener.md b/oppgavetekster/oving6/StockListener.md new file mode 100644 index 0000000..573f93f --- /dev/null +++ b/oppgavetekster/oving6/StockListener.md @@ -0,0 +1,64 @@ +# Observatør-observert-teknikken - StockListener-oppgave + +Denne oppgaven handler om å bruke observatør-observert-teknikken for å holde en aksjeindeks (`StockIndex`) informert om endringer i én eller flere aksjer (`Stock`). + +Observatør-observert-teknikken går ut på at det observerte objektet sier ifra til én eller flere observatører om at tilstanden er endret. I vårt tilfelle skal vi ta utgangspunkt i at aksjer (`Stock`) har en pris, og at personer eller institusjoner (`StockListener`) ønsker å holde seg oppdatert på aksjepriser. + +Alle filene i denne oppgaven skal lages i [oving6/stock](../../src/main/java/oving6/stock). + +## Del 1: Stock-klassen og StockListener-grensesnittet + +Du skal implementere en klasse `Stock` med følgende funksjonalitet: + +- `Stock(String ticker, double price)` - en konstruktør som tar inn en aksjekode (`ticker` ulik `null`) og en aksjepris. +- `void setPrice(double price)` - endringsmetode for aksjeprisen. Dersom aksjepris er negativ eller lik null, skal metoden utløse en `IllegalArgumentException`. +- `String getTicker()` - metode for å hente aksjekoden. +- `double getPrice()` - metode for å hente aksjeprisen. + +Du skal videre definere et lyttergrensesnitt kalt `StockListener`, som observatørene må implementere. Grensesnittet skal inneholde én metode: + +- `void stockPriceChanged(Stock stock, double oldValue, double newValue)` - lyttermetode for å holde lytteren oppdatert på aksjeprisen. Metoden skal ta inn et `Stock`-objekt, samt gammel og ny pris. Alle lyttere må implementere denne metoden. + +Foreløpig er `Stock` ikke observerbar. For at observatører skal kunne holdes oppdatert, må `Stock`-objekter administrere en liste med lyttere. Derfor må `Stock`-klassen i tillegg ha følgende metoder: + +- `void addStockListener(StockListener)` - metode for å legge til nye observatører. +- `void removeStockListener(StockListener)` - metode for å fjerne observatører. + +Observatørene skal holdes oppdatert på prisendringer. Derfor må lyttermetoden kalles hos alle registrerte observatører når aksjeprisen endres med `setPrice`-metoden. + +Testkode for denne oppgaven finner du her: [oving6/stock/StockTest.java](../../src/test/java/oving6/stock/StockTest.java). + +## Del 2: StockIndex implements StockListener + +Vi skal nå lage en veldig forenklet versjon av en aksjeindeks. I korte trekk bruker man en aksjeindeks til å måle utviklingen av et utvalg aksjer. Vår enkle, fiktive aksjeindeks `StockIndex` har et navn (`String`), indeks (`double`) og en liste med `Stock`-objektene som er inkludert i indeksen. Indeksen beregnes ut i fra aksjeprisene den "observerer", og vil være lik summen av disse. Når en av aksjeprisene øker eller synker, må tilstanden til `StockIndex`-objektet holdes konsistent med denne utviklingen. Dette lar seg gjøre ved at `StockIndex` observerer én eller flere `Stock`-objekter. Klassen skal ha følgende metoder: + +- `StockIndex(String name, Stock... stocks)` - konstruktør som tar inn ingen, én eller flere aksjer (`Stock`-objekter). `Stock`-parameteret defineres som et såkalt [varargs-parameter](https://www.ntnu.no/wiki/display/tdt4100/Varargs+-+variabelt+antall+argumenter). NB: `StockIndex`-objektet skal holdes oppdatert på aksjeprisene allerede fra det er instansiert. Dersom en indeks instansieres uten `Stock`-objekter, skal aksjeindeksen være `0`. Verken navnet eller noen av `Stock`-argumentene kan være `null`. +- `void addStock(Stock stock)` - metode for å legge til en aksjepris i indeksen. Argumentet kan ikke være `null`. +- `void removeStock(Stock stock)` - metode for å fjerne en aksjepris fra indeksen. +- `double getIndex()` - hentemetode for indeksen. + +I tillegg må `StockIndex`-klassen selvsagt implementere `StockListener` og dermed også lyttermetoden `stockPriceChanged`, slik at indeksen kan holdes oppdatert. + +Testkode for denne oppgaven finner du her: [oving6/stock/StockIndexTest.java](../../src/test/java/oving6/stock/StockIndexTest.java). + +## Ekstraoppgaver + +I en del sammenhenger vil du ikke være interessert i alle småendringer i en aksjepris, men interessert i endringer utenfor et visst område eller av en viss størrelse. Kanskje vil du kjøpe aksjer hvis det er billig nok, ønsker å selge dersom prisen blir høy nok eller ønsker å vite om større endringer som kan være signal om viktige prisendringer. Så for å unngå å sende ut mange uinteressante prisoppdateringer, er det aktuelt med to typer utvidelser av `Stock`-klassen. I begge tilfellene bruker men en egen `addStockListener`-metode for å registrere lytteren og hva slags endring man er interessert i. Implementér utvidelsen(e) i en subklasse som du kaller `SmartStock`. Merk at denne utvidelsen av `Stock` ikke er så relevant å bruke sammen med `StockIndex`, siden den da vil miste noen oppdateringer og dermed kunne risikere å være inkonsistent innimellom. + +### Pris*intervall* + +I denne utvidelsen skal du støtte lyttere som ønsker å få beskjed kun dersom `Stock`-objektets pris settes utenfor et gitt intervall. Følgende metode må implementeres: + +- `void addStockListener(StockListener, double min, double max)` - metode som legger til lyttere med krav til prisintervall. + +Lyttere som er registrert med denne metoden skal bare varsles dersom `Stock`-objektets pris endres til en verdi utenfor det angitte intervallet. Hint: Bruk en `Map`-felt til å holde oversikt over intervallene, eventuelt flere lister eller andre datastrukturer. + +### Pris*differanse* + +I denne utvidelsen skal du støtte lyttere som ønsker å få beskjed kun når akkumulerte endringer av `Stock`-objektets pris er større enn en gitt differanse. Følgende metode må implementeres: + +- `void addStockListener(StockListener, double difference)` - metode som legger til lyttere med krav til prisdifferanse. + +Et viktig poeng med denne er varianter er hvilke tidligere verdien som skal gis til lyttermetoden `stockPriceChanged` sitt andre argument. Denne verdien skal være den forrige verdien som ble rapportert, som kan være en annen enn den forrige prisverdien. Anta f.eks. at en lytter registreres med `10` som prisdifferanse og at aksjeprisen starter som `110` og så endres til `118` og videre til `121`. Da skal lyttermetoden `stockPriceChanged` kalles med `110` som gammel verdi og `121` som ny verdi, fordi dette sett fra lytterens perspektiv er forrige verdi den fikk vite om. En annen lytter som var registrert med prisdifferansen `5`, ville fått beskjed allerede ved første endring og da med `110` som gammel verdi og `118` som ny, men den ville ikke få beskjed om endringen fra `118` til `121`, fordi differansen da er for liten. Dersom prisen endrer seg videre til `124`, vil lytteren få beskjed og da med `118` som gammel verdi. + +Testkode for denne oppgaven finner du her: [oving6/stock/SmartStockTest.java](../../src/test/java/oving6/stock/SmartStockTest.java). diff --git a/oppgavetekster/oving6/img/highscore-list.png b/oppgavetekster/oving6/img/highscore-list.png new file mode 100644 index 0000000..7710f13 Binary files /dev/null and b/oppgavetekster/oving6/img/highscore-list.png differ diff --git a/oppgavetekster/oving6/img/highscore-list.puml b/oppgavetekster/oving6/img/highscore-list.puml new file mode 100644 index 0000000..fe1d890 --- /dev/null +++ b/oppgavetekster/oving6/img/highscore-list.puml @@ -0,0 +1,23 @@ +@startuml highscore-list + +skinparam dpi 400 + +class HighscoreList { + - int maxSize + - List results + + + HighscoreList(int) + + int size() + + int getElement(int) + + void addResult(int) + + void addHighscoreListListener(HighscoreListListener) + + void removeHighscoreListListener(HighscoreListListener) +} + +interface HighscoreListListener { + void listChanged(HighscoreList, int) +} + +HighscoreList -u-> "highscoreListListeners: *" HighscoreListListener + +@enduml diff --git a/oppgavetekster/oving6/img/hl-program.png b/oppgavetekster/oving6/img/hl-program.png new file mode 100644 index 0000000..3b6779c Binary files /dev/null and b/oppgavetekster/oving6/img/hl-program.png differ diff --git a/oppgavetekster/oving6/img/hl-program.puml b/oppgavetekster/oving6/img/hl-program.puml new file mode 100644 index 0000000..c3ab37f --- /dev/null +++ b/oppgavetekster/oving6/img/hl-program.puml @@ -0,0 +1,31 @@ +@startuml hl-program + +skinparam dpi 400 + +class HighscoreList { + - int maxSize + - List results + + + HighscoreList(int) + + int size() + + int getElement(int) + + void addResult(int) + + void addHighscoreListListener(HighscoreListListener) + + void removeHighscoreListListener(HighscoreListListener) +} + +interface HighscoreListListener { + void listChanged(HighscoreList, int) +} + +class HighscoreListProgram { + - HighscoreList highscoreList + + + void init() + + void run() +} + +HighscoreList -u-> "highscoreListListeners: *" HighscoreListListener +HighscoreListProgram ..l|> HighscoreListListener : "\t\t" + +@enduml diff --git a/src/main/java/oving6/highscorelist/.gitkeep b/src/main/java/oving6/highscorelist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/oving6/logger/.gitkeep b/src/main/java/oving6/logger/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/oving6/office/.gitkeep b/src/main/java/oving6/office/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/oving6/stock/.gitkeep b/src/main/java/oving6/stock/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/oving6/highscorelist/HighscoreListTest.java b/src/test/java/oving6/highscorelist/HighscoreListTest.java new file mode 100644 index 0000000..92601dc --- /dev/null +++ b/src/test/java/oving6/highscorelist/HighscoreListTest.java @@ -0,0 +1,160 @@ +package oving6.highscorelist; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class HighscoreListTest { + + private int pos1; + private int pos2; + private HighscoreList highscoreList; + + private static void checkHighscoreList(String contextMessage, HighscoreList list, + List elements) { + assertEquals(elements.size(), list.size(), + contextMessage + " -> Testing the length of the highscore list"); + + int i = 0; + + for (int element : elements) { + assertEquals(element, list.getElement(i), + contextMessage + " -> Testing that the element at position " + i + " matches"); + + i++; + } + } + + private void addResultWithListener(int pos, int element) { + pos1 = pos; + highscoreList.addResult(element); + + // Check that the position that was changed is the same as the one sent to the listener + assertEquals(pos1, pos2, "Added " + element + " at position " + pos + + " -> Testing the position received by the listener"); + } + + @BeforeEach + public void setUp() { + highscoreList = new HighscoreList(3); + pos1 = -1; + pos2 = -1; + } + + @Test + @DisplayName("Test constructor") + public void testConstructor() { + assertEquals(0, highscoreList.size(), "Testing initialization of the highscore list"); + } + + @Test + @DisplayName("Add results (simple)") + public void testAddElementSimple() { + highscoreList.addResult(5); + HighscoreListTest.checkHighscoreList("Added 5 to an empty list", highscoreList, List.of(5)); + + highscoreList.addResult(6); + HighscoreListTest.checkHighscoreList("Added 6 to the list [5]", highscoreList, + List.of(5, 6)); + + highscoreList.addResult(2); + HighscoreListTest.checkHighscoreList("Added 2 to the list [5, 6]", highscoreList, + List.of(2, 5, 6)); + } + + @Test + @DisplayName("Add results - list becomes too long") + public void testAddElementMoreThanMax() { + highscoreList.addResult(5); + highscoreList.addResult(6); + highscoreList.addResult(2); + HighscoreListTest.checkHighscoreList("Added 5, 6, and 2 to the list", highscoreList, + List.of(2, 5, 6)); + + highscoreList.addResult(3); + HighscoreListTest.checkHighscoreList("Added 3 to the list [2, 5, 6]", highscoreList, + List.of(2, 3, 5)); + + highscoreList.addResult(7); + HighscoreListTest.checkHighscoreList("Added 7 to the list [2, 3, 5]", highscoreList, + List.of(2, 3, 5)); + } + + @Test + @DisplayName("Add two identical elements") + public void testAddElementDuplicate() { + highscoreList.addResult(5); + highscoreList.addResult(6); + highscoreList.addResult(2); + HighscoreListTest.checkHighscoreList("Added 5, 6, and 2 to the list", highscoreList, + List.of(2, 5, 6)); + + highscoreList.addResult(2); + HighscoreListTest.checkHighscoreList("Added 2 to the list [2, 5, 6]", highscoreList, + List.of(2, 2, 5)); + } + + @Test + @DisplayName("Test listeners (simple)") + public void testListListenersSimple() { + // Mock a listener + HighscoreListListener listener = (list, pos) -> pos2 = pos; + highscoreList.addHighscoreListListener(listener); + + this.addResultWithListener(0, 5); + HighscoreListTest.checkHighscoreList("Added 5 to the list []", highscoreList, List.of(5)); + + this.addResultWithListener(1, 6); + HighscoreListTest.checkHighscoreList("Added 6 to the list [5]", highscoreList, + List.of(5, 6)); + + this.addResultWithListener(0, 2); + HighscoreListTest.checkHighscoreList("Added 2 to the list [5, 6]", highscoreList, + List.of(2, 5, 6)); + } + + @Test + @DisplayName("With listener - list becomes too long") + public void testListListenerMoreThanMax() { + // Mock a listener + HighscoreListListener listener = (list, pos) -> pos2 = pos; + highscoreList.addHighscoreListListener(listener); + + highscoreList.addResult(5); + highscoreList.addResult(6); + highscoreList.addResult(2); + HighscoreListTest.checkHighscoreList("Added 5, 6, and 2 to the list", highscoreList, + List.of(2, 5, 6)); + + this.addResultWithListener(1, 3); + HighscoreListTest.checkHighscoreList("Added 3 to the list [2, 5, 6]", highscoreList, + List.of(2, 3, 5)); + + // Reset pos2 since the next element falls outside the list and is therefore not updated by + // itself and sent to the listener + pos2 = -1; + this.addResultWithListener(-1, 7); + HighscoreListTest.checkHighscoreList("Added 7 to the list [2, 3, 5]", highscoreList, + List.of(2, 3, 5)); + } + + @Test + @DisplayName("With listener - two identical elements") + public void testListListenerDuplicate() { + // Mock a listener + HighscoreListListener listener = (list, pos) -> pos2 = pos; + highscoreList.addHighscoreListListener(listener); + + highscoreList.addResult(5); + highscoreList.addResult(6); + highscoreList.addResult(2); + HighscoreListTest.checkHighscoreList("Added 5, 6, and 2 to the list", highscoreList, + List.of(2, 5, 6)); + + this.addResultWithListener(1, 2); + HighscoreListTest.checkHighscoreList("Added 2 to the list [2, 5, 6]", highscoreList, + List.of(2, 2, 5)); + } +} diff --git a/src/test/java/oving6/logger/DistributingLoggerTest.java b/src/test/java/oving6/logger/DistributingLoggerTest.java new file mode 100644 index 0000000..13d7486 --- /dev/null +++ b/src/test/java/oving6/logger/DistributingLoggerTest.java @@ -0,0 +1,100 @@ +package oving6.logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.ByteArrayOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class DistributingLoggerTest { + + private static final String formatString = "%s: %s (%s)"; + + private ByteArrayOutputStream infoStream; + private ByteArrayOutputStream warnStream; + private ByteArrayOutputStream errorStream; + private DistributingLogger logger; + private StreamLogger infoLogger; + private StreamLogger warnLogger; + private StreamLogger errorLogger; + + @BeforeEach + public void setUp() { + infoStream = new ByteArrayOutputStream(); + warnStream = new ByteArrayOutputStream(); + errorStream = new ByteArrayOutputStream(); + + infoLogger = new StreamLogger(infoStream); + warnLogger = new StreamLogger(warnStream); + errorLogger = new StreamLogger(errorStream); + + logger = new DistributingLogger(errorLogger, warnLogger, infoLogger); + } + + @Test + @DisplayName("Log to INFO") + public void testLogToInfo() { + infoLogger.setFormatString(formatString); + logger.log(ILogger.INFO, "Dette er en info-melding.", null); + assertEquals(String.format(formatString, ILogger.INFO, "Dette er en info-melding.", null), + infoStream.toString().trim(), "Test the format of the info message after logging"); + assertEquals("", warnStream.toString(), + "Test the warning stream after logging an info message"); + assertEquals("", errorStream.toString(), + "Test the error stream after logging an info message"); + } + + @Test + @DisplayName("Log to WARNING") + public void testLogToWarn() { + warnLogger.setFormatString(formatString); + logger.log(ILogger.WARNING, "Dette er en advarsel.", null); + assertEquals(String.format(formatString, ILogger.WARNING, "Dette er en advarsel.", null), + warnStream.toString().trim(), + "Test the format of the warning message after logging"); + assertEquals("", infoStream.toString(), + "Test the info stream after logging a warning message"); + assertEquals("", errorStream.toString(), + "Test the error stream after logging a warning message"); + } + + @Test + @DisplayName("Log to ERROR") + public void testLogToError() { + Exception exception = new IllegalStateException(); + errorLogger.setFormatString(formatString); + logger.log(ILogger.ERROR, "Dette er en feilmelding.", exception); + assertEquals( + String.format(formatString, ILogger.ERROR, "Dette er en feilmelding.", exception), + errorStream.toString().trim(), + "Test the format of the error message after logging"); + assertEquals("", warnStream.toString(), + "Test the warning stream after logging an error message"); + assertEquals("", infoStream.toString(), + "Test the info stream after logging an error message"); + } + + @Test + @DisplayName("Change info logger") + public void testChangeInfoLogger() { + ByteArrayOutputStream newInfoStream = new ByteArrayOutputStream(); + StreamLogger newInfoLogger = new StreamLogger(newInfoStream); + + infoLogger.setFormatString(formatString); + logger.log(ILogger.INFO, "Dette er en info-melding.", null); + assertEquals(String.format(formatString, ILogger.INFO, "Dette er en info-melding.", null), + infoStream.toString().trim(), "Test the format of the info message after logging"); + + newInfoLogger.setFormatString(formatString); + logger.setLogger(ILogger.INFO, newInfoLogger); + logger.log(ILogger.INFO, "Dette er den andre info-meldingen.", null); + assertEquals(String.format(formatString, ILogger.INFO, "Dette er en info-melding.", null), + infoStream.toString().trim(), + "Test the first info stream after switching the info logger and logging another info message"); + assertEquals( + String.format(formatString, ILogger.INFO, "Dette er den andre info-meldingen.", + null), + newInfoStream.toString().trim(), + "Test the second info stream after switching the info logger and logging a new info message"); + } +} diff --git a/src/test/java/oving6/logger/FilteringLoggerTest.java b/src/test/java/oving6/logger/FilteringLoggerTest.java new file mode 100644 index 0000000..314fd7e --- /dev/null +++ b/src/test/java/oving6/logger/FilteringLoggerTest.java @@ -0,0 +1,86 @@ +package oving6.logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class FilteringLoggerTest { + + private ByteArrayOutputStream stream; + private StreamLogger subLogger; + + @BeforeEach + public void setUp() { + stream = new ByteArrayOutputStream(); + subLogger = new StreamLogger(stream); + } + + @Test + @DisplayName("Create logger without severities") + public void testFilteringLoggerNoSeverities() { + FilteringLogger logger = new FilteringLogger(subLogger); + assertFalse(logger.isLogging(ILogger.INFO), + "Test logger without severity for info-logging"); + assertFalse(logger.isLogging(ILogger.WARNING), + "Test logger without severity for warning-logging"); + assertFalse(logger.isLogging(ILogger.ERROR), + "Test logger without severity for error-logging"); + } + + @Test + @DisplayName("Create logger with error and info") + public void testFilteringLoggerErrorAndInfo() { + FilteringLogger logger = new FilteringLogger(subLogger, ILogger.ERROR, ILogger.INFO); + assertTrue(logger.isLogging(ILogger.INFO), "Test error and info logger for info-logging"); + assertFalse(logger.isLogging(ILogger.WARNING), + "Test error and info logger for warning-logging"); + assertTrue(logger.isLogging(ILogger.ERROR), "Test error and info logger for error-logging"); + } + + @Test + @DisplayName("Create logger with warning, set error") + public void testFilteringLoggerWarningSetIsLoggingError() { + FilteringLogger logger = new FilteringLogger(subLogger, ILogger.WARNING); + assertFalse(logger.isLogging(ILogger.INFO), "Test warning logger for info-logging"); + assertTrue(logger.isLogging(ILogger.WARNING), "Test warning logger for warning-logging"); + assertFalse(logger.isLogging(ILogger.ERROR), "Test warning logger for error-logging"); + + logger.setIsLogging(ILogger.ERROR, true); + assertFalse(logger.isLogging(ILogger.INFO), + "Set error-logging for warning logger and test for info-logging"); + assertTrue(logger.isLogging(ILogger.WARNING), + "Set error-logging for warning logger and test for warning-logging"); + assertTrue(logger.isLogging(ILogger.ERROR), + "Set error-logging for warning logger and test for error-logging"); + } + + @Test + @DisplayName("Logger with severity ERROR") + public void testErrorLogging() { + IllegalStateException exception = new IllegalStateException(); + FilteringLogger logger = new FilteringLogger(subLogger, ILogger.ERROR); + String formatString = "%s: %s (%s)"; + + subLogger.setFormatString(formatString); + logger.log(ILogger.ERROR, "Noe er feil!", exception); + assertEquals(String.format(formatString, ILogger.ERROR, "Noe er feil!", exception), + stream.toString().trim(), "Test the format of error message after logging"); + } + + @Test + @DisplayName("Logger with severity INFO in ERROR logger") + public void testInfoToErrorLogger() { + IllegalStateException exception = new IllegalStateException(); + FilteringLogger logger = new FilteringLogger(subLogger, ILogger.ERROR); + String formatString = "%s: %s (%s)"; + + subLogger.setFormatString(formatString); + logger.log(ILogger.INFO, "Noe er feil!", exception); + assertEquals("", stream.toString(), + "Test the stream after logging info message in error logger"); + } +} diff --git a/src/test/java/oving6/logger/StreamLoggerTest.java b/src/test/java/oving6/logger/StreamLoggerTest.java new file mode 100644 index 0000000..c6f6155 --- /dev/null +++ b/src/test/java/oving6/logger/StreamLoggerTest.java @@ -0,0 +1,49 @@ +package oving6.logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.ByteArrayOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class StreamLoggerTest { + + private static final String formatString = "%s: %s (%s)"; + private static final String melding = "En melding ble logget!"; + + private ByteArrayOutputStream stream; + private StreamLogger logger; + + @BeforeEach + public void setUp() { + stream = new ByteArrayOutputStream(); + logger = new StreamLogger(stream); + } + + @Test + @DisplayName("Logs info message") + public void testLog() { + logger.log(ILogger.INFO, melding, null); + assertTrue(stream.toString().contains(melding), + "Test that the stream contains the message after it has been logged"); + assertTrue(stream.toString().contains(ILogger.INFO), + "Test that the stream contains a message of type info after it has been logged"); + } + + @Test + @DisplayName("Logs exception") + public void testLogException() { + assertThrows(IllegalArgumentException.class, () -> { + logger.setFormatString(null); + }, "Test that IllegalArgumentException is thrown when the format is null"); + + IllegalStateException exception = new IllegalStateException(); + logger.setFormatString(formatString); + logger.log(ILogger.INFO, melding, exception); + assertEquals(String.format(formatString, ILogger.INFO, melding, exception), + stream.toString().trim(), + "Test the format of the message that was logged with an exception"); + } +} diff --git a/src/test/java/oving6/office/ClerkTest.java b/src/test/java/oving6/office/ClerkTest.java new file mode 100644 index 0000000..97cc747 --- /dev/null +++ b/src/test/java/oving6/office/ClerkTest.java @@ -0,0 +1,77 @@ +package oving6.office; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ClerkTest { + + private Clerk clerk; + private Printer printer; + + @BeforeEach + public void setUp() { + printer = new Printer(); + clerk = new Clerk(printer); + } + + @Test + @DisplayName("Perform calculations") + public void testDoCalculations() { + assertEquals(0, clerk.getTaskCount(), + "Testing the initialization of the number of tasks performed"); + + double calc1 = clerk.doCalculations((x, y) -> x + y, 2.0, 3.5); + assertEquals(5.5, calc1, "Testing calculation with addition: 2.0 + 3.5"); + assertEquals(1, clerk.getTaskCount(), "Testing the number of tasks after 1 calculation"); + + double calc2 = clerk.doCalculations((x, y) -> x / y, 2.0, 4.0); + assertEquals(0.5, calc2, "Testing calculation with division: 2.0/4.0"); + assertEquals(2, clerk.getTaskCount(), "Testing the number of tasks after two calculations"); + } + + @Test + @DisplayName("Print documents") + public void testPrintDocuments() { + assertEquals(0, clerk.getTaskCount(), + "Testing the initialization of the number of tasks performed"); + + // Print a document + clerk.printDocument("document1"); + assertTrue(printer.getPrintHistory(clerk).contains("document1"), + "Testing if the document that was printed was added to the printer history"); + assertEquals(1, clerk.getTaskCount(), "Testing the number of tasks after 1 print"); + assertEquals(1, printer.getPrintHistory(clerk).size(), + "Testing the number of prints in the history after 1 print"); + + // Print another document + clerk.printDocument("document2"); + assertTrue(printer.getPrintHistory(clerk).contains("document2"), + "Testing if document 2 that was printed was added to the printer history"); + assertEquals(2, clerk.getTaskCount(), "Testing the number of tasks after 2 prints"); + assertEquals(2, printer.getPrintHistory(clerk).size(), + "Testing the number of prints in the history after 2 prints"); + } + + @Test + @DisplayName("Retrieve task count") + public void testTaskCount() { + assertEquals(0, clerk.getTaskCount(), + "Testing the initialization of the number of tasks performed"); + + clerk.printDocument("document1"); + assertEquals(1, clerk.getTaskCount(), "Testing the number of tasks after 1 print"); + + clerk.doCalculations((x, y) -> x + y, 2.0, 3.5); + assertEquals(2, clerk.getTaskCount(), + "Testing the number of tasks after 1 print and 1 calculation"); + } + + @Test + @DisplayName("Retrieve resource count") + public void testResourceCount() { + assertEquals(1, clerk.getResourceCount(), "Testing the number of resources for one worker"); + } +} diff --git a/src/test/java/oving6/office/ManagerTest.java b/src/test/java/oving6/office/ManagerTest.java new file mode 100644 index 0000000..710a220 --- /dev/null +++ b/src/test/java/oving6/office/ManagerTest.java @@ -0,0 +1,123 @@ +package oving6.office; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ManagerTest { + + private Clerk clerk; + private Manager manager; + private Printer printer; + + @BeforeEach + public void setUp() { + printer = new Printer(); + clerk = new Clerk(printer); + manager = new Manager(List.of(clerk)); + } + + @Test + @DisplayName("Create Manager with empty Employee collection") + public void testNoEmployeesConstructor() { + assertThrows(IllegalArgumentException.class, () -> { + new Manager(new ArrayList()); + }, "Test creating a new Manager with an empty Employee collection"); + } + + @Test + @DisplayName("Check getResourceCount without middle managers") + public void testResourceCount() { + assertEquals(2, manager.getResourceCount(), + "Test the number of resources for a manager without middle managers"); + } + + @Test + @DisplayName("Check getResourceCount with middle managers") + public void testMiddleManagementResourceCount() { + Manager topManager = new Manager(List.of(manager)); + assertEquals(3, topManager.getResourceCount(), + "Test the number of resources with middle managers"); + } + + @Test + @DisplayName("Perform a calculation") + public void testDoCalculations() { + assertEquals(0, clerk.getTaskCount(), + "Test the initialization of the number of tasks for a worker"); + assertEquals(0, manager.getTaskCount(), + "Test the initialization of the number of tasks for a manager"); + + double calc = manager.doCalculations((x, y) -> x + y, 2.0, 3.5); + assertEquals(5.5, calc, "Test calculation with addition: 2.0 + 3.5"); + + assertEquals(1, clerk.getTaskCount(), + "Test the number of tasks for a worker after 1 calculation"); + assertEquals(1, manager.getTaskCount(), + "Test the number of tasks for a manager after 1 calculation"); + } + + @Test + @DisplayName("Print a document") + public void testPrintDocuments() { + manager.printDocument("document1"); + assertTrue(printer.getPrintHistory(clerk).contains("document1"), + "Test that the printer history contains the document that was printed"); + assertEquals(1, clerk.getTaskCount(), + "Test the number of tasks for a worker after 1 print"); + assertEquals(1, manager.getTaskCount(), + "Test the number of tasks for a manager after 1 print"); + assertEquals(1, printer.getPrintHistory(clerk).size(), + "Test the number of prints in the printer history after 1 print"); + } + + @Test + @DisplayName("Check task count") + public void testTaskCount() { + assertEquals(0, clerk.getTaskCount(), + "Test the initialization of the number of tasks for a worker"); + assertEquals(0, manager.getTaskCount(), + "Test the initialization of the number of tasks for a manager"); + + manager.printDocument("document"); + assertEquals(1, clerk.getTaskCount(), + "Test the number of tasks for a worker after 1 print"); + assertEquals(1, manager.getTaskCount(), + "Test the number of tasks for a manager after 1 print"); + + manager.doCalculations((x, y) -> x + y, 2.0, 3.5); + assertEquals(2, clerk.getTaskCount(), + "Test the number of tasks for a worker after 1 print and 1 calculation"); + assertEquals(2, manager.getTaskCount(), + "Test the number of tasks for a manager after 1 print and 1 calculation"); + } + + @Test + @DisplayName("Multiple clerks") + public void testSeveralClerks() { + Clerk secondClerk = new Clerk(printer); + Manager multiManager = new Manager(List.of(clerk, secondClerk)); + assertEquals(1, clerk.getResourceCount(), "Test the number of resources for worker #1"); + assertEquals(3, multiManager.getResourceCount(), + "Test the number of resources for a manager with two workers"); + assertEquals(1, secondClerk.getResourceCount(), "Test the resources for worker #2"); + + multiManager.printDocument("document"); + assertEquals(1, multiManager.getTaskCount(), + "Test the number of tasks for a manager after 1 print"); + assertTrue( + (clerk.getTaskCount() == 1 || secondClerk.getTaskCount() == 1) + && (clerk.getTaskCount() == 0 || secondClerk.getTaskCount() == 0), + "Test that only one of the workers has performed a task after 1 print"); + + double calc = multiManager.doCalculations((x, y) -> x + y, 2.0, 3.5); + assertEquals(5.5, calc, "Test calculation with addition: 2.0 + 3.5"); + assertEquals(2, multiManager.getTaskCount(), + "Test the number of tasks for a manager after 1 print and 1 calculation"); + } +} diff --git a/src/test/java/oving6/office/PrinterTest.java b/src/test/java/oving6/office/PrinterTest.java new file mode 100644 index 0000000..d119c83 --- /dev/null +++ b/src/test/java/oving6/office/PrinterTest.java @@ -0,0 +1,97 @@ +package oving6.office; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PrinterTest { + + private Clerk clerk1; + private Clerk clerk2; + private Printer printer; + + @BeforeEach + public void setUp() { + printer = new Printer(); + clerk1 = new Clerk(printer); + clerk2 = new Clerk(printer); + } + + @Test + @DisplayName("Testing printing") + public void testPrintDocument() { + assertThrows(IllegalArgumentException.class, () -> { + printer.printDocument(null, clerk1); + }, "Test that an exception is thrown when the document is null"); + + assertThrows(IllegalArgumentException.class, () -> { + printer.printDocument("document1", null); + }, "Test that an exception is thrown when the worker is null"); + } + + @Test + @DisplayName("Only one printer printing") + public void testPrintSingleClerk() { + assertEquals(0, printer.getPrintHistory(clerk1).size(), + "Test initialization of the number of prints in the printer history"); + + printer.printDocument("document1", clerk1); + assertEquals(1, printer.getPrintHistory(clerk1).size(), + "Test the number of prints in the history after 1 print"); + assertTrue(printer.getPrintHistory(clerk1).contains("document1"), + "Test that the document that was printed is in the history"); + + printer.printDocument("document2", clerk1); + assertEquals(2, printer.getPrintHistory(clerk1).size(), + "Test the number of prints in the history after 2 prints"); + assertTrue(printer.getPrintHistory(clerk1).contains("document2"), + "Test that document 2 that was printed is in the history"); + } + + @Test + @DisplayName("Multiple printers printing") + public void testPrintMultipleClerks() { + assertEquals(0, printer.getPrintHistory(clerk1).size(), + "Test initialization of printer history for worker1"); + assertEquals(0, printer.getPrintHistory(clerk2).size(), + "Test initialization of printer history for worker2"); + + // Print document for Clerk 1 + printer.printDocument("document1", clerk1); + assertEquals(1, printer.getPrintHistory(clerk1).size(), + "Test the number of prints in the history for worker1 after printing 1 document"); + assertTrue(printer.getPrintHistory(clerk1).contains("document1"), + "Test that the history of worker1 contains the document that was printed"); + assertEquals(0, printer.getPrintHistory(clerk2).size(), + "Test the number of prints in the history for worker2 after worker1 has printed " + + "a document"); + + // Print document for Clerk 2 + printer.printDocument("document2", clerk2); + assertEquals(1, printer.getPrintHistory(clerk2).size(), + "Test the number of prints in the history for worker2 after printing a document"); + assertTrue(printer.getPrintHistory(clerk2).contains("document2"), + "Test that the history of worker2 contains the document that was printed"); + assertEquals(1, printer.getPrintHistory(clerk1).size(), + "Test the number of prints in the history for worker1 after both worker1 and " + + "worker2 have printed 1 document each"); + } + + @Test + @DisplayName("Modifying printer history is not allowed") + public void testPrintHistoryModification() { + printer.printDocument("document1", clerk1); + printer.printDocument("document2", clerk1); + assertEquals(2, printer.getPrintHistory(clerk1).size(), + "Test the number of prints in the history after 2 prints"); + + // Remove a document and check that the printer history remains unchanged + printer.getPrintHistory(clerk1).remove("document1"); + assertEquals(2, printer.getPrintHistory(clerk1).size(), + "Test the number of prints in the history after 2 prints and attempting to " + + "remove 1 of them"); + } +} diff --git a/src/test/java/oving6/stock/SmartStockTest.java b/src/test/java/oving6/stock/SmartStockTest.java new file mode 100644 index 0000000..1624f38 --- /dev/null +++ b/src/test/java/oving6/stock/SmartStockTest.java @@ -0,0 +1,124 @@ +package oving6.stock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class SmartStockTest { + + private SmartStock stock; + private double oldPrice; + private double newPrice; + + // Used to check that listeners work + private double oldPriceListener; + private double newPriceListener; + + private void setPriceForListener(double oldPrice, double newPrice) { + oldPriceListener = oldPrice; + newPriceListener = newPrice; + } + + private void setPriceCheckListener(String contextMessage, double newPrice, + double expectedOldPrice, double expectedNewPrice) { + // Update the price + this.oldPrice = this.newPrice; + this.newPrice = newPrice; + stock.setPrice(newPrice); + + // Check that the listener has received the change + assertEquals(expectedOldPrice, this.oldPriceListener, + contextMessage + " -> Test old price for listener after updating price from " + + oldPrice + " to " + newPrice); + assertEquals(expectedNewPrice, this.newPriceListener, + contextMessage + " -> Test new price for listener after updating price from " + + oldPrice + " to " + newPrice); + } + + @BeforeEach + public void setUp() { + stock = new SmartStock("APPL", 110.0); + } + + @Test + @DisplayName("Test constructor") + public void testConstructor() { + assertEquals("APPL", stock.getTicker(), "Test ticker"); + assertEquals(110.0, stock.getPrice(), "Test stock price"); + } + + @Test + @DisplayName("Negative stock price throws error") + public void testSetNegativePrice() { + assertThrows(IllegalArgumentException.class, () -> { + stock.setPrice(-20.0); + }, "Test setting negative stock price"); + } + + @Test + @DisplayName("Stock price equal to zero throws error") + public void testSetZeroPrice() { + assertThrows(IllegalArgumentException.class, () -> { + stock.setPrice(0); + }, "Test setting stock price equal to zero"); + } + + @Test + @DisplayName("Add listener") + public void testStockListener() { + StockListener listener = (Stock stock, double oldPrice, double newPrice) -> this + .setPriceForListener(oldPrice, newPrice); + stock.addStockListener(listener); + + this.setPriceCheckListener("Listener on all", 118.0, 110.0, 118.0); + assertEquals(118.0, stock.getPrice(), "Test stock price after updating price"); + + this.setPriceCheckListener("Listener on all", 121.0, 118.0, 121.0); + assertEquals(121.0, stock.getPrice(), "Test stock price after updating price twice"); + } + + @Test + @DisplayName("Test listener on price interval") + public void testIntervalListener() { + StockListener listener = (Stock stock, double oldPrice, double newPrice) -> this + .setPriceForListener(oldPrice, newPrice); + stock.addStockListener(listener, 110.0, 120.0); + + // Price within the interval does not notify the listener + this.setPriceCheckListener("Listener on price interval", 118.0, 0.0, 0.0); + assertEquals(118.0, stock.getPrice(), "Test stock price after updating price"); + + // Price outside the interval notifies the listener + this.setPriceCheckListener("Listener on price interval", 121.0, 118.0, 121.0); + assertEquals(121.0, stock.getPrice(), + "Test stock price after updating price for the second time"); + + // Price within the interval does not notify the listener (expected values remain unchanged) + this.setPriceCheckListener("Listener on price interval", 115.0, 118.0, 121.0); + assertEquals(115.0, stock.getPrice(), + "Test stock price after updating price for the third time"); + } + + @Test + @DisplayName("Test listener on difference") + public void testDifferenceListener() { + StockListener listener = (Stock stock, double oldPrice, double newPrice) -> this + .setPriceForListener(oldPrice, newPrice); + stock.addStockListener(listener, 10.0); + + // Price with a difference less than 10 does not notify the listener + this.setPriceCheckListener("Listener on difference", 118.0, 0.0, 0.0); + assertEquals(118.0, stock.getPrice()); + + // Price with a difference greater than 10 notifies the listener + this.setPriceCheckListener("Listener on difference", 121.0, 110.0, 121.0); + assertEquals(121.0, stock.getPrice()); + + // Price with a difference less than 10 does not notify the listener (expected values remain + // unchanged) + this.setPriceCheckListener("Listener on difference", 115.0, 110.0, 121.0); + assertEquals(115.0, stock.getPrice()); + } +} diff --git a/src/test/java/oving6/stock/StockIndexTest.java b/src/test/java/oving6/stock/StockIndexTest.java new file mode 100644 index 0000000..4ec9564 --- /dev/null +++ b/src/test/java/oving6/stock/StockIndexTest.java @@ -0,0 +1,116 @@ +package oving6.stock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class StockIndexTest { + + private static final double applePrice = 534.98; + private static final double facebookPrice = 67.80; + private static final double epsilon = 0.000001; + + private Stock apple; + private Stock facebook; + private StockIndex index0; + private StockIndex index1; + private StockIndex indexN; + + @BeforeEach + public void setUp() { + apple = new Stock("AAPL", applePrice); + facebook = new Stock("FB", facebookPrice); + + index0 = new StockIndex("OSEBX"); + index1 = new StockIndex("OSEBX", facebook); + indexN = new StockIndex("OSEBX", facebook, apple); + } + + @Test + @DisplayName("Test constructor") + public void testConstructor() { + assertEquals(0.0, index0.getIndex(), epsilon, "Test the value of the index with 0 stocks"); + assertEquals(facebookPrice, index1.getIndex(), epsilon, + "Test the value of the index with 1 stock"); + assertEquals(facebookPrice + applePrice, indexN.getIndex(), epsilon, + "Test the value of the index with 2 stocks"); + + assertThrows(IllegalArgumentException.class, () -> { + new StockIndex(null); + }, "Test constructor with null name"); + + assertThrows(IllegalArgumentException.class, () -> { + new StockIndex("OSEBX", apple, null, facebook); + }, "Test constructor with null stocks"); + } + + @Test + @DisplayName("Add stock") + public void testAddStock() { + assertEquals(0.0, index0.getIndex(), epsilon, "Test the value of the index with 0 stocks"); + + index0.addStock(facebook); + assertEquals(facebookPrice, index0.getIndex(), epsilon, + "Test the value of the index after adding 1 stock"); + + assertThrows(IllegalArgumentException.class, () -> { + index0.addStock(null); + }, "Test adding null stock"); + } + + @Test + @DisplayName("Add the same stock twice") + public void testAddDuplicateStocks() { + assertEquals(0.0, index0.getIndex(), epsilon, "Test the value of the index with 0 stocks"); + + index0.addStock(facebook); + assertEquals(facebookPrice, index0.getIndex(), epsilon, + "Test the value of the index after adding 1 stock"); + + index0.addStock(facebook); + assertEquals(facebookPrice, index0.getIndex(), epsilon, + "Test the value of the index after adding a stock that is already in the index"); + } + + @Test + @DisplayName("Remove stock") + public void testRemoveStock() { + assertEquals(facebookPrice + applePrice, indexN.getIndex(), epsilon, + "Test the value of the index with 2 stocks"); + + indexN.removeStock(apple); + assertEquals(facebookPrice, indexN.getIndex(), epsilon, + "Test the value of the index after removing 1 stock"); + + indexN.removeStock(apple); + assertEquals(facebookPrice, indexN.getIndex(), epsilon, + "Test the value of the index after removing 1 stock that was not in the index"); + + indexN.removeStock(facebook); + assertEquals(0.0, indexN.getIndex(), epsilon, + "Test the value of the index after removing the only stock in the index"); + } + + @Test + @DisplayName("Change stock price") + public void testChangePrice() { + double facebookPrice2 = 67.0; + double facebookPrice3 = 69.0; + + facebook.setPrice(facebookPrice2); + assertEquals(facebookPrice2, index1.getIndex(), epsilon, + "Test the value of the index with 1 stock after changing the stock price"); + assertEquals(facebookPrice2 + applePrice, indexN.getIndex(), epsilon, + "Test the value of the index with 2 stocks after changing the price of 1 stock"); + + facebook.setPrice(facebookPrice3); + assertEquals(facebookPrice3, index1.getIndex(), epsilon, + "Test the value of the index with 1 stock after changing the stock price a " + + "second time"); + assertEquals(facebookPrice3 + applePrice, indexN.getIndex(), epsilon, + "Test the value of the index with 2 stocks after changing the price of 1 stock a " + + "second time"); + } +} diff --git a/src/test/java/oving6/stock/StockTest.java b/src/test/java/oving6/stock/StockTest.java new file mode 100644 index 0000000..2be7f3a --- /dev/null +++ b/src/test/java/oving6/stock/StockTest.java @@ -0,0 +1,89 @@ +package oving6.stock; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class StockTest { + + private Stock stock; + private double oldPrice; + private double newPrice; + + // Used to check that listeners work + private double oldPriceListener; + private double newPriceListener; + + private void setPriceForListener(double oldPrice, double newPrice) { + oldPriceListener = oldPrice; + newPriceListener = newPrice; + } + + private void setPriceCheckListener(double newPrice, double expectedOldPrice, + double expectedNewPrice) { + // Update the price + this.oldPrice = this.newPrice; + this.newPrice = newPrice; + stock.setPrice(newPrice); + + // Check that the listener has received the change + assertEquals(expectedOldPrice, this.oldPriceListener, + "Test old price for listener after updating price from " + oldPrice + " to " + + newPrice); + assertEquals(expectedNewPrice, this.newPriceListener, + "Test new price for listener after updating price from " + oldPrice + " to " + + newPrice); + } + + @BeforeEach + public void setUp() { + stock = new Stock("APPL", 110.0); + oldPrice = 0.0; + newPrice = 110.0; + oldPriceListener = 0.0; + newPriceListener = 0.0; + } + + @Test + @DisplayName("Test constructor") + public void testConstructor() { + assertEquals("APPL", stock.getTicker(), "Test ticker"); + assertEquals(110.0, stock.getPrice(), "Test stock price"); + + assertThrows(IllegalArgumentException.class, () -> { + new Stock(null, 110.0); + }, "Test constructor with null ticker"); + } + + @Test + @DisplayName("Negative stock price throws error") + public void testSetNegativePrice() { + assertThrows(IllegalArgumentException.class, () -> { + stock.setPrice(-20.0); + }, "Test setting negative stock price"); + } + + @Test + @DisplayName("Stock price equal to zero throws error") + public void testSetZeroPrice() { + assertThrows(IllegalArgumentException.class, () -> { + stock.setPrice(0); + }, "Test setting stock price equal to zero"); + } + + @Test + @DisplayName("Add listener") + public void testStockListener() { + StockListener listener = (Stock stock, double oldPrice, double newPrice) -> this + .setPriceForListener(oldPrice, newPrice); + stock.addStockListener(listener); + + this.setPriceCheckListener(118.0, 110.0, 118.0); + assertEquals(118.0, stock.getPrice(), "Test stock price after updating price"); + + this.setPriceCheckListener(121.0, 118.0, 121.0); + assertEquals(121.0, stock.getPrice(), "Test stock price after updating price twice"); + } +}