Stop asinfo-gen when the as.csv download breaks off (closes #177) #178
@@ -33,6 +33,9 @@ tested.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-27: `make asupdate` stops with an error when the download of `as.csv`
|
||||||
|
breaks off. The generator used to retry the failed read forever. A record that
|
||||||
|
does not parse is still logged and skipped (closes #177)
|
||||||
- 2026-10-27: `make asupdate` runs the new `script/asupdate`. It replaces the AS
|
- 2026-10-27: `make asupdate` runs the new `script/asupdate`. It replaces the AS
|
||||||
list built into RouteWatch only when the generator succeeds and finds at least
|
list built into RouteWatch only when the generator succeeds and finds at least
|
||||||
one AS. A failed download used to replace the list with an empty file. The
|
one AS. A failed download used to replace the list with an empty file. The
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ func main() {
|
|||||||
// parseCSV reads as.csv from r and returns its ASes, with the handle and the
|
// parseCSV reads as.csv from r and returns its ASes, with the handle and the
|
||||||
// description as the file spells them. A header whose first columns are not
|
// description as the file spells them. A header whose first columns are not
|
||||||
// the AS number, the handle and the description is an error. A record that
|
// the AS number, the handle and the description is an error. A record that
|
||||||
// does not parse is logged and skipped. A file left with no AS is an error.
|
// does not parse is logged and skipped. A read that fails, such as on a broken
|
||||||
|
// download, is an error. A file left with no AS is an error.
|
||||||
func parseCSV(r io.Reader) ([]ASInfo, error) {
|
func parseCSV(r io.Reader) ([]ASInfo, error) {
|
||||||
reader := csv.NewReader(r)
|
reader := csv.NewReader(r)
|
||||||
|
|
||||||
@@ -109,12 +110,18 @@ func parseCSV(r io.Reader) ([]ASInfo, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if _, ok := errors.AsType[*csv.ParseError](err); ok {
|
||||||
log.Printf("Error reading CSV record: %v", err)
|
log.Printf("Error reading CSV record: %v", err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any other error comes from the download, such as a broken
|
||||||
|
// connection. Every later read returns it again.
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read CSV record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
asn, err := strconv.Atoi(record[0])
|
asn, err := strconv.Atoi(record[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Invalid ASN %q: %v", record[0], err)
|
log.Printf("Invalid ASN %q: %v", record[0], err)
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/iotest"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// parseTimeLimit is how long a test waits for parseCSV to return before it
|
||||||
|
// fails.
|
||||||
|
const parseTimeLimit = 2 * time.Second
|
||||||
|
|
||||||
// TestParseCSVReadsTodaysLayout parses an as.csv with the country code column
|
// TestParseCSVReadsTodaysLayout parses an as.csv with the country code column
|
||||||
// that the source added after the description. It expects each AS with its
|
// that the source added after the description. It expects each AS with its
|
||||||
// handle and description spelled exactly as in the file, accents included.
|
// handle and description spelled exactly as in the file, accents included.
|
||||||
@@ -71,3 +78,65 @@ func TestParseCSVRejectsAFileWithNoASes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestParseCSVSkipsARecordThatDoesNotParse parses an as.csv with a record
|
||||||
|
// that has too few fields between two valid records. It expects both valid
|
||||||
|
// ASes and no error, so that one bad record does not stop make asupdate.
|
||||||
|
func TestParseCSVSkipsARecordThatDoesNotParse(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const asCSV = "asn,handle,description,country-code\n" +
|
||||||
|
"1,LVLT-1,Level 3 Parent LLC,US\n" +
|
||||||
|
"2,UDEL-DCN\n" +
|
||||||
|
"3,MIT-GATEWAYS,Massachusetts Institute of Technology,US\n"
|
||||||
|
|
||||||
|
got, err := parseCSV(strings.NewReader(asCSV))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseCSV: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []ASInfo{
|
||||||
|
{ASN: 1, Handle: "LVLT-1", Description: "Level 3 Parent LLC"},
|
||||||
|
{
|
||||||
|
ASN: 3,
|
||||||
|
Handle: "MIT-GATEWAYS",
|
||||||
|
Description: "Massachusetts Institute of Technology",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slices.Equal(got, want) {
|
||||||
|
t.Errorf("parseCSV = %+v, want %+v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseCSVStopsWhenTheDownloadBreaksOff parses an as.csv whose download
|
||||||
|
// breaks off after the first record. From then on every read fails, as on a
|
||||||
|
// broken connection. It expects parseCSV to return that error, so that make
|
||||||
|
// asupdate keeps the old list.
|
||||||
|
func TestParseCSVStopsWhenTheDownloadBreaksOff(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const asCSV = "asn,handle,description,country-code\n" +
|
||||||
|
"1,LVLT-1,Level 3 Parent LLC,US\n"
|
||||||
|
|
||||||
|
download := io.MultiReader(
|
||||||
|
strings.NewReader(asCSV), iotest.ErrReader(io.ErrUnexpectedEOF))
|
||||||
|
|
||||||
|
// parseCSV runs on its own goroutine, so that a parseCSV that never
|
||||||
|
// returns fails the test instead of hanging it.
|
||||||
|
parsed := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := parseCSV(download)
|
||||||
|
parsed <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-parsed:
|
||||||
|
if !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
t.Errorf("parseCSV = %v, want %v", err, io.ErrUnexpectedEOF)
|
||||||
|
}
|
||||||
|
case <-time.After(parseTimeLimit):
|
||||||
|
t.Fatalf("parseCSV did not return within %v", parseTimeLimit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user