|
| 1 | +package probe |
| 2 | + |
| 3 | +// scan_periface_test.go — #1338: el scan de vecinos se lanzaba como UN solo |
| 4 | +// comando (`iw dev` + bucle sobre TODAS las interfaces) con UN solo timeout |
| 5 | +// de 10 s. Si una interfaz escanea lento (p. ej. una radio 5 GHz en canal |
| 6 | +// DFS, scan pasivo canal a canal), el timeout mataba el bucle entero y la |
| 7 | +// salida parcial —con las interfaces que SÍ habían escaneado— se descartaba: |
| 8 | +// la unidad no reportaba NINGÚN vecino (UniFi 6 Lite, mt7621). Ahora el scan |
| 9 | +// va por interfaz, cada una con su propio presupuesto de tiempo: una que |
| 10 | +// falla o se cuelga deja su sección vacía y no arrastra a las demás. |
| 11 | + |
| 12 | +import ( |
| 13 | + "context" |
| 14 | + "strings" |
| 15 | + "testing" |
| 16 | + "time" |
| 17 | +) |
| 18 | + |
| 19 | +// runnerScanParcial simula una unidad con dos radios donde el scan de la |
| 20 | +// primera interfaz (5 GHz) muere por timeout y la segunda (2.4 GHz) devuelve |
| 21 | +// un vecino real. |
| 22 | +type runnerScanParcial struct { |
| 23 | + fakeRunner |
| 24 | +} |
| 25 | + |
| 26 | +func (r runnerScanParcial) Run(ctx context.Context, cmd string, timeout time.Duration) (string, error) { |
| 27 | + if cmd == CmdScanIface("phy1-ap0") { |
| 28 | + return "", &fakeErr{cmd} // timeout del runner real: err y sin salida |
| 29 | + } |
| 30 | + return r.fakeRunner.Run(ctx, cmd, timeout) |
| 31 | +} |
| 32 | + |
| 33 | +// TestScanPorInterfazSobreviveAFalloParcial: con el scan de la primera |
| 34 | +// interfaz fallando, los vecinos de la segunda deben llegar igual. |
| 35 | +func TestScanPorInterfazSobreviveAFalloParcial(t *testing.T) { |
| 36 | + iwDev := "phy#1\n" + |
| 37 | + "\tInterface phy1-ap0\n" + |
| 38 | + "\t\taddr 62:22:32:5b:5c:99\n" + |
| 39 | + "\t\tssid I1_LAN\n" + |
| 40 | + "\t\ttype AP\n" + |
| 41 | + "\t\tchannel 60 (5300 MHz), width: 80 MHz, center1: 5290 MHz\n" + |
| 42 | + "phy#0\n" + |
| 43 | + "\tInterface phy0-ap0\n" + |
| 44 | + "\t\taddr 60:22:32:5b:5c:98\n" + |
| 45 | + "\t\tssid I1_LAN\n" + |
| 46 | + "\t\ttype AP\n" + |
| 47 | + "\t\tchannel 5 (2432 MHz), width: 20 MHz, center1: 2432 MHz\n" |
| 48 | + // Salida real del scan en la U6 Lite del reportero (#1338), con las dos |
| 49 | + // líneas "last seen" (la de cabecera con [boottime] y la de señal). |
| 50 | + scan2g := "BSS c4:eb:39:0c:2b:c1(on phy0-ap0)\n" + |
| 51 | + "\tlast seen: 275247.799s [boottime]\n" + |
| 52 | + "\tTSF: 901954671988 usec (10d, 10:32:34)\n" + |
| 53 | + "\tfreq: 2437.0\n" + |
| 54 | + "\tbeacon interval: 100 TUs\n" + |
| 55 | + "\tcapability: ESS (0x1011)\n" + |
| 56 | + "\tsignal: -67.00 dBm\n" + |
| 57 | + "\tlast seen: 2140 ms ago\n" + |
| 58 | + "\tSSID: Vecino_24\n" |
| 59 | + run := runnerScanParcial{fakeRunner{outs: map[string]string{ |
| 60 | + CmdIwDev: iwDev, |
| 61 | + CmdScanIface("phy0-ap0"): scan2g, |
| 62 | + }}} |
| 63 | + p := NewProber(run, Options{}) |
| 64 | + |
| 65 | + pl := p.Build(context.Background(), "u6lite", "test") |
| 66 | + if pl.Data.Wireless == nil { |
| 67 | + t.Fatal("sección wireless ausente") |
| 68 | + } |
| 69 | + scans := pl.Data.Wireless.Scans |
| 70 | + if len(scans) != 1 { |
| 71 | + t.Fatalf("el fallo del scan 5 GHz no debe arrastrar al 2.4 GHz: %+v", scans) |
| 72 | + } |
| 73 | + sc := scans[0] |
| 74 | + if sc.Iface != "phy0-ap0" || sc.BSSID != "C4:EB:39:0C:2B:C1" || sc.SSID != "Vecino_24" || sc.Channel != 6 || sc.Signal != -67 { |
| 75 | + t.Fatalf("vecino mal parseado: %+v", sc) |
| 76 | + } |
| 77 | + // Los BSSIDs propios salen del MISMO `iw dev` (sin segundo spawn). |
| 78 | + if len(pl.Data.Wireless.OwnBssids) != 2 { |
| 79 | + t.Fatalf("ownBssids perdidos: %+v", pl.Data.Wireless.OwnBssids) |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +// TestScanPorInterfazSinIw: sin `iw` en el equipo, ni scans ni BSSIDs propios |
| 84 | +// (best-effort, como antes). |
| 85 | +func TestScanPorInterfazSinIw(t *testing.T) { |
| 86 | + p := NewProber(fakeRunner{outs: map[string]string{}}, Options{}) |
| 87 | + pl := p.Build(context.Background(), "sin-iw", "test") |
| 88 | + if pl.Data.Wireless != nil && len(pl.Data.Wireless.Scans) != 0 { |
| 89 | + t.Fatalf("sin iw no puede haber scans: %+v", pl.Data.Wireless.Scans) |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +// TestCmdScanIface: el comando por interfaz cita el nombre (como hacía el |
| 94 | +// bucle viejo con "$i"). |
| 95 | +func TestCmdScanIface(t *testing.T) { |
| 96 | + cmd := CmdScanIface("phy0-ap0") |
| 97 | + if !strings.Contains(cmd, `iw dev "phy0-ap0" scan`) { |
| 98 | + t.Fatalf("comando inesperado: %q", cmd) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +// TestScanPorInterfazThrottle: el throttle (scanDue) sigue gobernando el |
| 103 | +// scan aunque ahora se lance por interfaz: 2 interfaces = 2 comandos en la |
| 104 | +// primera Build y 0 en la segunda (dentro del intervalo). |
| 105 | +func TestScanPorInterfazThrottle(t *testing.T) { |
| 106 | + iwDev := "phy#0\n" + |
| 107 | + "\tInterface phy0-ap0\n\t\taddr 60:22:32:5b:5c:98\n\t\ttype AP\n" + |
| 108 | + "\tInterface phy0-ap1\n\t\taddr 62:22:32:5b:5c:98\n\t\ttype AP\n" |
| 109 | + run := &countingRunner{fakeRunner: fakeRunner{outs: map[string]string{ |
| 110 | + CmdIwDev: iwDev, |
| 111 | + }}} |
| 112 | + p := NewProber(run, Options{}) |
| 113 | + ctx := context.Background() |
| 114 | + |
| 115 | + p.Build(ctx, "rt", "test") |
| 116 | + if run.scanCalls != 2 { |
| 117 | + t.Fatalf("primera Build debería escanear las 2 interfaces, scanCalls=%d", run.scanCalls) |
| 118 | + } |
| 119 | + p.Build(ctx, "rt", "test") |
| 120 | + if run.scanCalls != 2 { |
| 121 | + t.Fatalf("segunda Build dentro del intervalo NO debería escanear, scanCalls=%d", run.scanCalls) |
| 122 | + } |
| 123 | + p.ForceScan() |
| 124 | + p.Build(ctx, "rt", "test") |
| 125 | + if run.scanCalls != 4 { |
| 126 | + t.Fatalf("ForceScan debería reescanear las 2 interfaces, scanCalls=%d", run.scanCalls) |
| 127 | + } |
| 128 | +} |
0 commit comments