Quand’ero giovane e ingenuo e dovevo testare del codice che faceva uso di liste (in senso lato: liste, array, set, stringhe di elementi separati da un certo carattere, ecc.), spesso consideravo solo i casi di liste nulle o con 0, 1 o 2 elementi. Davo per scontato che, se il codice funzionava per 2 elementi, allora doveva funzionare anche per >2 elementi.
Poi un giorno di fine marzo 2020 avvenne l’impensabile.
Lo ricordo come fosse ieri. Stavo lavorando con quella schifezza immonda chiamata javascript. Nel backend node.jx c’era una funzione che trasformava una stringa di ID separati da virgola in un’array di ID numerici, per poi fare delle query (gli ID separati da trattino arrivavano da un CSV che veniva uploadato, per quello non erano già da subito in un array). Ad un certo punto, sorse la necessità di aggiungere un endpoint che riceveva nella query string un elenco di ID. C’era poco tempo per completare la modifica, per cui invece di verificare se si possono usare le virgole nella query string o fare l’url encoding della stringa, pensai che fosse più veloce usare i trattini al posto delle virgole e poi fare stringa_con_virgole = stringa_con_trattini.replace('-', ',')
. In C#, Java e persino PHP metodi e funzioni con quel nome sostituiscono tutte le occorrenze del needle con il replacement. Sarà lo stesso in javascript, no?
Testo la modifica con 0, 1 e 2 elementi nella query string, e funziona come previsto. Viene rilasciata la nuova versione e passo ad altro.
Settimane dopo, il cliente si lamenta che, quando fa una richiesta con più di 2 ID, l’operazione viene svolta solo per i primi 2. Segue il panico generale. Le query vengono eseguite in transazione, e viene fatto rollback della transazione se viene lanciata un’eccezione in qualsiasi punto della business logic, o addirittura se c’è una uncaught exception (in node si può gestire a livello globale un evento che viene lanciato in tal caso). Com’è possibile che l’operazione venga solo parzialmente eseguita?
Ebbene, dopo diverse ore di debug e ricerca, scoprii che non veniva generato nessun errore durante l’esecuzione, ma il bug era dovuto a quella maledizione che da quasi trent’anni affligge l’umanità e che porta il nome di javascript.
A differenza dei linguaggi sani di mente (e di PHP), replace
in javascript sostituisce tutte le occorrenze solo se si passa una regex con il flag g
come needle. Altrimenti sostituisce solo la prima occorrenza. Altrimenti, per sostituire tutte le occorrenze usando una stringa come needle si può usare replaceAll
.
L’altra follia è che parseInt, la funzione che usavo per convertire gli ID da stringhe a numeri, fa davvero di tutto per estrarre un numero dalla stringa. Se la stringa (dopo aver eliminato gli spazi all’inizio) inizia con un numero e poi contiene altri caratteri, i caratteri non numerici vengono ignorati e viene ritornato il numero trovato all’inizio della stringa. Quindi, ad esempio, parseInt('1-2-3')
ritorna 1
.
Quindi la replace
con la stringa come needle sostituiva la prima virgola con il trattino; split
ritornava un array che conteneva due elementi, il primo con il primo ID, e il secondo con gli altri ID ancora separati da trattino; infine, parseInt
convertiva entrambi gli elementi in interi senza dare errori, ma il secondo conteneva solo il primo ID presente in quella stringa. Quindi ottenevo sempre 2 ID numerici quando passavo >=2 ID nella query string.
Il bug descritto sopra non sarebbe mai giunto in produzione se avessi testato con almeno 3 ID. In particolare, consiglio di testare le liste sia con 2 che con 3 elementi, così se il test con 2 passa ma quello con 3 fallisce, si può ragionevolmente supporre che il problema sia qualche operazione di trasformazione della lista che non viene eseguita iterativamente.