Compare commits
1
Commits
main
...
7e1c6f9598
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1c6f9598 |
@@ -33,6 +33,9 @@ tested.
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
@@ -85,7 +85,8 @@ func main() {
|
||||
// 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
|
||||
// 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) {
|
||||
reader := csv.NewReader(r)
|
||||
|
||||
@@ -109,12 +110,18 @@ func parseCSV(r io.Reader) ([]ASInfo, error) {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*csv.ParseError](err); ok {
|
||||
log.Printf("Error reading CSV record: %v", err)
|
||||
|
||||
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])
|
||||
if err != nil {
|
||||
log.Printf("Invalid ASN %q: %v", record[0], err)
|
||||
|
||||
@@ -2,11 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
"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
|
||||
// that the source added after the description. It expects each AS with its
|
||||
// handle and description spelled exactly as in the file, accents included.
|
||||
@@ -71,3 +78,35 @@ func TestParseCSVRejectsAFileWithNoASes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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