### Autori - Fabio Marchesi, matricola: 844941. - Simone Mallei, matricola: 844659. Questa documentazione si riferisce al seguente [repository](https://gitlab.com/Fabio11111999/2021_assignment1_multiprocessing_stress_tester). # Multiprocessing Stress Tester Multiprocessing Stress Tester è uno strumento, realizzato principalmente in Python, che permette di controllare la correttezza di un programma C++ confrontandone gli output con quelli prodotti da un altro programma C++ ritenuto corretto, su molti input diversi. Questo tool diventa particolarmente utile per verificare la correttezza di un codice con tempi di esecuzione molto ridotti ma contenente eventuali errori confrontandone i risultati con quelli prodotti da una soluzione più lenta ma sicuramente corretta. I risultati ottenuti dall'utilizzo del tester sono salvati all'interno di un database (condiviso tra tutte le istanze del pacchetto) e possono essere visualizzati tramite lo stesso portale web tramite il quale viene utilizzato il tester stesso. ### Input Il tester prende come input: - ```correct.cpp```: il sorgente ritenuto corretto, legge in input da ```stdin``` e scrive i propri risultati su ```stdout```. - ```wrong.cpp```: il sorgente del quale si vuole verificare la correttezza, legge in input da ```stdin``` e scrive i propri risultati su ```stdout```. - ```checker.cpp```: prende come parametri 2 stringhe contenenti rispettivamente il nome del file di testo in cui è contenuto l'output di ```correct.cpp``` ed il nome del file di testo nel quale è contenuto l'output di ```wrong.cpp```, tale programma si occupa di stabilire se l'output di ```wrong.cpp``` può essere considerato corretto confrontandolo con quello di ```correct.cpp```, nel più semplice dei casi basta controllare che i contenuti dei due file corrispondano, ma in altri potrebbero essere richieste verifiche maggiori. Nel caso in cui il verdetto di ```checker.cpp``` sia positivo allora tale codice dovrà restituire 0 (esecuzione terminata correttamente), altrimenti un valore diverso da 0 (esecuzione terminata senza successo). - ```generator.cpp```: prende come parametro una stringa rappresentante un possibile seed da utilizzare per la generazione del testcase corrente, e stampa su ```stdout``` un'istanza sulla quale verranno confrontati i risultati di ```wrong.cpp``` e ```correct.cpp```. - ```TL```: rappresenta il tempo limite in secondi entro il quale dovrà terminare l'esecuzione di ```wrong.cpp```, particolarmente utile quando si vuole cercare una particolare istanza (corner-case) che se utilizzata come input implica che l'esecuzione di ```wrong.cpp``` non termini mai. - ```TC```: il numero di istanze utilizzate per verificare la correttezza. - ```safe```: un valore booleano il cui valore positivo indica che ```wrong.cpp``` verrà compilata con delle particolari flags ([sanitizers](https://docs.microsoft.com/en-us/cpp/sanitizers/?view=msvc-160)) che permetteno di rilevare errori a runtime. Utilizzando tale opzione è possibile rilevare un insieme più ampio di errori ma verranno rallentati sostanzialmente i tempi d'esecuzione. ### Output L'output prodotto dal tester sarà uno dei seguenti: - 0: L'output prodotto da ```wrong.cpp``` risulta corretto per tutte le istanze provate, ed il suo tempo d'esecuzione non ha mai superato il time limit. - 1: Compilazione di ```generator.cpp``` fallita. - 2: Compilazione di ```checker.cpp``` fallita. - 3: Compilazione di ```wrong.cpp``` fallita. - 4: Compilazione di ```correct.cpp``` fallita. - 5: Esecuzione di ```generator.cpp``` fallita. - 6: Esecuzione di ```correct.cpp``` fallita. - 7: Esecuzione di ```wrong.cpp``` fallita. - 8: L'esecuzione di ```wrong.cpp``` ha superato il tempo limite per almeno un'istanza di input. - 9: Viene trovato un'istanza per la quale ```wrong.cpp``` produce un output non considerato corretto rispetto a quello di ```correct.cpp```, ciò succede quando l'esecuzione di ```checker.cpp``` fallisce. Nel caso in cui il resoconto sia rappresentato da uno dei valori compresi tra 1 e 7 allora verranno forniti dettagli riguardo il motivo di tale errore (l'errore di compilazione nel caso si tratti di compilazione fallita, il contenuto di ```stderr``` nel caso in cui si tratti di un'esecuzione fallita). Nel caso in cui si ottenga come output il valore 8 verrà fornita l'istanza di input sulla quale l'esecuzione di ```wrong.cpp``` ha sforato il time limit ed l'output prodotto da ```correct.cpp``` su tale istanza. Infine se l'output del tester corrisponde a 9 allora verranno forniti: l'istanza di input sulla quale l'esecuzione di ```wrong.cpp``` ha prodotto un output non considerato corretto, l'output prodotto da ```wrong.cpp``` e l'output prodotto da ```correct.cpp```. ### Funzionamento Per funzionare velocemente il tester utilizza un numero di processi (che chiameremo workers) pari al numero di core della cpu sulla quale viene eseguito, tali processi si divideranno equamente il numero di istanze da testare e possono comunicare tra loro tramite una flag binaria condivisa. Il tester si occupa inizialmente della compilazione di ```generator.cpp```, ```checker.cpp```, ```correct.cpp``` e ```wrong.cpp``` ed una volta ottenuto il binario ne crea un copia per ogni singolo worker che lavorerà in una sua cartella indipendente. A questo punto ogni worker inizia a generare e controllare i risultati prodotti per un determinato numero di istanze (```TC```/ numero di workers), nel caso in cui venga trovata un'istanza per la quale l'output di ```wrong.cpp``` non viene considerato corretto allora l'esecuzione di tale worker termina e tramite la flag binaria condivisa vengono avvisati anche gli altri workers in modo che a loro volta possano terminare la propria esecuzione. Per effettuale l'esecuzione dei vari file binari necessari viene fatto un uso intensivo del modulo [subprocess](https://docs.python.org/3/library/subprocess.html), in particolare della funzione ```subprocess.run()``` Nonostante siano stati mantenuti tutti i riferimenti al progetto come "Multiprocessing Stress Tester", in realtà l'applicativo preso in considerazione dalla pipeline non risulta multiprocesso. A causa di restrizioni imposte dal servizio di hosting di [Heroku](https://www.heroku.com/) che impediscono di spawnare più sottoprocessi e in virtù del fatto che il focus del progetto sia incentrato sulla pipeline e non sull'applicativo, è stato deciso di utilizzare un singolo processo: in questo modo l'applicazione non risulta più effettivamente multiprocesso, ma mantiene le funzionalità del progetto iniziale (l'utilizzo di un processo unico comporta dei tempi d'esecuzione leggermente peggiori nel tester). ### Applicazione Web La versione web di questo strumento è rappresentata attraverso 2 possibili schermate: - New Submission: è la schermata che permette di effettuare uno stresstest impostando gli input dello strumento: inviata la richiesta, l'applicazione eseguirà lo stresstest con le informazioni ricevute e, una volta completata la procedura, restituirà un messaggio di sottoposizione completata in caso positivo ed il risultato verrà memorizzato all'interno del database di stresstesting (che risulta comune). - Show Submissions: permette di osservare le informazioni sulle ultime 10 sottoposizioni memorizzate all'interno del database dell'applicazione, permettendo di visualizzare il risultato del debugger su ogni stresstest e di scaricare i file sorgente di input. ## Pipeline ### Build Lo stage di build si occupa di preparare tutte le risorse che saranno successivamente utilizzate dalla pipeline. I pacchetti necessari sono specificati all'interno del file ```requirements.txt``` e per ognuno di essi è specificata anche la versione utilizzata. Sarà quindi sufficiente richiamare il comando ```pip install``` per fare l'installazione di tutte le risorse. Le risorse installate saranno poi caricate all'interno della cache in modo che sia possibile ottenerle velocemente durante gli stages successivi. ### Verify Lo stage di verify si occupa di effettuare un controllo statico dei codici del pacchetto sviluppato. Questo permette di rilevare potenziali errori oppure il mancato utilizzo di convezioni senza dover eseguire il codice. La fase di verify è attuata tramite due jobs: - ```verify_prospector```: si occupa di verificare che [prospector](http://prospector.landscape.io/en/master/) non rilevi errori statici nel codice prodotto. - ```verify_bandit```: si occupa di verificare che [bandit](https://pypi.org/project/bandit/0.13.1/) ritenga il codice prodotto come consono. Controllando staticamente il codice ci si assicura della mancanza di possibili errori che potrebbero rappresentare vulnerabilità del pacchetto stesso. ### Unit Test Lo stage di unit test consiste nel sottoporre il codice prodotto ad una serie di possibili istanze verificandone il giusto funzionamento. Nel nostro caso, per assicurarci del corretto funzionamento del tester, vengono effettuati una serie di test nei quali vengono utilizzati dei sorgenti di prova per poi confrontare gli output del tester con quelli che ci si aspettebbero utilizzando tali sorgenti. Per effettuare tale stage viene utilizzato il modulo [pytest](https://docs.pytest.org/en/6.2.x/) : un framework che localizza automaticamente i test scritti secondo una certa sintassi, li esegue e ne riporta i risultati. L'esecuzione di questi test ci permette di verificare automaticamente che le funzioni del tester non siano state compromesse durante l'ultimo commit. ### Integration Test Lo stage di integration test si occupa di verificare il corretto funzionamento e la corretta interazione dei vari componenti dell'applicativo, nel nostro caso ciò significa verificare che il tester ed il database all'interno del quale vengono memorizzati gli esiti dei vari test collaborino correttamente. Per effettuare tale verifica viene utilizzato lo stess tester per effettuare un nuovo test su dei sorgenti di prova per poi effettuare una query al database per verificare se i risultati attesi sono presenti all'interno di esso. Anche in questo caso è stato utilizzato il modulo [pytest](https://docs.pytest.org/en/6.2.x/) per l'esecuzione automatica dei test. ### Package Lo stage di package è una fase che prevede la creazione del modulo relativo all'applicazione che si vuole distribuire. Questo consiste in due componenti: [Source Distribution](https://docs.python.org/3/distutils/sourcedist.html) e [Built Distribution](https://docs.python.org/3/distutils/builtdist.html). - [Source Distribution](https://docs.python.org/3/distutils/sourcedist.html): è un archivio che contiene i file sorgenti che fanno parte del pacchetto al quale fanno riferimento. - [Built Distribution](https://docs.python.org/3/distutils/builtdist.html): può essere un file binario o un eseguibile di installazione che permette di installare un pacchetto in modo più semplice, veloce ed accessibile in generale (in questo caso un file ```.whl```). Per poter creare il package sono necessari i file ```pyproject.toml```, ```setup.cfg``` e ```Manifest.in```, che permettono di configurare informazioni come le dipendenze, la versione del modulo per la corretta impostazione dell'applicazione e il file da includere nel modulo. È stato deciso di utilizzare il modulo [build](https://pypi.org/project/build/) in quanto permette la creazione del [Source Distribution](https://docs.python.org/3/distutils/sourcedist.html) e della [Built Distribution](https://docs.python.org/3/distutils/builtdist.html) utilizzando rispettivamente le keyword ```--sdist``` e ```--wheel```. Entrambi i file vengono generati sotto la directory ```dist``` e per questo viene creato l'artefatto del percorso ```src/main/dist```, in modo tale che il contenuto venga mantenuto per la fase successiva della pipeline, ovvero lo stage di release. ### Release Questa fase della pipeline prevede il rilascio dell'applicazione, ovvero nel nostro caso specifico il rilascio del modulo [multiprocessing-stress-tester](https://pypi.org/project/multiprocessing-stress-tester/) nel ```Python Package Index```: la piattaforma ufficiale per il rilascio dei propri package. Per effettuare l'upload nell'indice [PyPI](https://pypi.org/) è stato utilizzato [twine](https://pypi.org/project/twine/): attraverso il richiamo di questo modulo vengono rilasciate le distribuzioni generate nella fase di package. Per completare la fase di release, prima bisogna indicare lo username e la password del proprio account su [PyPI](https://pypi.org/) e queste due variabili sono definite nel CI/CD Settings rispettivamente come ```TWINE_USERNAME``` e ```TWINE_PASSWORD``` (visto l'utizzo di [twine](https://pypi.org/project/twine/)). ### Deploy Lo stage finale della pipeline implementata è quello di deploy, che permette di distribuire il modulo rilasciato in una piattaforma cloud (in questo caso [Heroku](https://www.heroku.com/)). Heroku esegue l'hosting dell'applicazione e del database relativo all'applicazione ([PostgreSQL](https://www.postgresql.org/docs/)): per effettuare il deploy della nuova versione del modulo è necessaria una modifica del container che contiene l'applicazione. Date le necessità di avere dipendenze specifiche da utilizzare all'interno dell'applicazione (ad esempio ```g++``` per la compilazione dei programmi C++) è stato deciso di effettuare il deploy costruendo l'immagine [Docker](https://www.docker.com/), utilizzando quindi il relativo ```Dockerfile``` che contiene tutte le istruzioni necessarie a far funzionare in modo corretto il container. Nell'ambiente Python dell'immagine Docker vengono installati il package rilasciato durante la fase di release [multiprocessing-stress-tester](https://pypi.org/project/multiprocessing-stress-tester/) e [gunicorn==20.1.0](https://pypi.org/project/gunicorn/20.1.0/), ovvero tutti i moduli python necessari per il corretto funzionamento dell'applicazione sulla piattaforma di hosting scelta. All'interno dell'immagine vengono installate anche le dipendenze per poter effettuare una connessione ```SSH``` al container distribuito: questo può permettere, in un'eventuale fase di monitoring, di potersi collegare direttamente all'immagine in esecuzione per poter effettuare operazioni di troubleshooting su eventuali problemi riscontrati all'interno dell'applicazione.