Perché?

Durante il mio lavoro quotidiano mi capita a volte di dover fare il debug di job di test openQA falliti.

Uno dei mantra del testing è riprodurre il problema e per questo compito la community di openQA ha sviluppato alcuni strumenti.

In pratica, ho spesso un output come questo qui sotto proveniente da alcune operazioni di clonazione dei job:

Cloning parents of sle-15-SP4-Server-DVD-Updates-x86_64-Build20250112-1-fips_ker_mode_gnome@64bit
1 job has been created:
 - sle-15-SP4-Server-DVD-Updates-x86_64-Build20250112-1-fips_ker_mode_gnome@64bit -> https://openqa.suse.de/tests/16425390
Cloning parents of sle-15-SP5-Server-DVD-Updates-x86_64-Build20250112-1-fips_ker_mode_gnome@64bit
1 job has been created:
 - sle-15-SP5-Server-DVD-Updates-x86_64-Build20250112-1-fips_ker_mode_gnome@64bit -> https://openqa.suse.de/tests/16425391
Cloning parents of sle-15-SP4-Server-DVD-Updates-x86_64-Build20250112-1-fips_ker_mode_gnome@64bit
1 job has been created:
 - sle-15-SP4-Server-DVD-Updates-x86_64-Build20250112-1-fips_ker_mode_gnome@64bit -> https://openqa.suse.de/tests/16425392

E quando voglio monitorare quei job, dovrei copiare e incollare tutti gli URL dei job e passarli come argomenti alla fantastica utilità openqa-mon, che mostrerà e mi notificherà lo stato dei job nel terminale.

$ openqa-mon https://openqa.suse.de/tests/16425390+2

Immagina di dover monitorare 50 job openQA contemporaneamente. Copiare e incollare manualmente ogni URL dall’output della console in openqa-mon richiede tempo ed è soggetto a errori. Questo diventa rapidamente un collo di bottiglia nel mio workflow.

Inizio

Sebbene openQA offra un’interfaccia web per monitorare i job, preferisco il workflow basato su terminale di openqa-mon per la sua flessibilità e le sue capacità di scripting. Tuttavia, anche con openqa-mon, raccogliere manualmente gli URL rimane un punto dolente.

Da persona pigra, mi chiedo sempre: posso automatizzarlo? Ogni volta che mi ritrovo a fare la stessa cosa due o tre volte. Ovviamente sì. Lo faremo in Rust 🦀? Beh, perché no? Forse imparerò qualcosa nel processo 😄

$ cargo init oqa-jobfilter

crab-shell Crediti immagine a: @taryn-elliott

Il progetto completo è disponibile su GitHub ed è sotto licenza MIT.

Definizione del problema

  1. Il programma dovrebbe comportarsi come un filtro per la shell, accettando l’input tramite stdin e producendo l’output tramite stdout: $ openqa-clone-job <myjobs> | oqa-jobfilter
  2. Il programma deve essere testabile: voglio svilupparlo utilizzando un processo di sviluppo Test-Driven, che mi permetta di modificarne il design e l’architettura interna pur mantenendo lo stesso comportamento
  3. L’output deve essere ordinato e pronto per essere passato così com’è a un’invocazione di openqa-mon
  4. L’output deve essere il più compatto possibile; ad esempio, quando ho ID di test consecutivi come https://openqa.suse.de/tests/1201, https://openqa.suse.de/tests/1202, https://openqa.suse.de/tests/1203, https://openqa.suse.de/tests/1204 posso semplicemente inviare 1201+3 a openqa-mon. Allo stesso modo, diversi ID di job per la stessa istanza openQA possono essere raggruppati separandoli da virgole, quindi test clonati come https://openqa.suse.de/tests/1201, https://openqa.suse.de/tests/1207, https://openqa.suse.de/tests/1210, https://openqa.suse.de/tests/1215 dovrebbero diventare
openqa-mon https://openqa.suse.de 1201,1207,1210,1215

Dettagli di implementazione

Il concetto di Traits in Rust è essenziale per soddisfare i requisiti #1 e #2. Questo significa che non scriveremo una funzione che richiede un parametro di un tipo specifico, ma accetteremo qualsiasi tipo che implementi quei comportamenti di Read/Write. Questo è simile alle interfacce di Go (or alle classi astratte nei linguaggi orientati agli oggetti) ed è un paradigma di programmazione molto potente.

Quindi la nostra funzione main leggerà e scriverà da stdin/stdout, mentre la funzione di calcolo vera e propria leggerà/scriverà semplicemente da/su un lettore/scrittore “generico”. In questo modo possiamo anche testare la funzione passando input fittizi e ispezionando gli output.

pub fn process_input<R: Read, W: Write>(input: R, mut output: W) -> io::Result<()> {

Requisito #3: L’ordinamento, la de-duplicazione e la formattazione sono gestiti da funzionalità incluse nella ricca libreria standard di Rust.

Il requisito #4 è il più complesso: per implementare il controllo degli ID consecutivi e il raggruppamento per lo stesso dominio dobbiamo memorizzare ogni job in una struttura dati adeguata

pub struct OpenQAJob {
    pub domain: Domain,
    pub id: u32,
    pub consecutive_count: u32,
}

che a questo punto merita di essere inserita in un file sorgente separato. È un’ottima occasione per imparare come organizzare un progetto Rust e modellare i “Domain Objects”. Nota che a ogni OpenQAJob sono associate delle funzioni (molto simili a dei “metodi”).

Funzionalità interessanti

  • Ho cercato di utilizzare anche alcune funzionalità del linguaggio Rust:

    • valutazione delle costanti a tempo di compilazione
    • il codice è organizzato e suddiviso in file sorgente logicamente separati
    • il linter “clippy” è configurato per essere il più pignolo possibile
    • commenti di documentazione: possiamo estrarre facilmente la documentazione direttamente dal codice sorgente
    • unit testing per coprire tutti i casi e consentire un refactoring senza timori
  • Come bonus, ho aggiunto una GitHub action per eseguire gli unit test e compilare il progetto a ogni commit+push; pronto per un ciclo di release appropriato.

Conclusioni

La creazione di questo programma è stata una sessione di hacking rapida e senza troppi fronzoli, quindi ci sono sicuramente opportunità di miglioramento: se vuoi contribuire, sentiti libero di contattarmi e/o segnalare problemi (issue) o inviare pull request. Buon divertimento!