blob: 8a7decf3d783127932057cda3654f52c727a196f [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package wifi
import (
"context"
"fmt"
"net"
"time"
"chromiumos/tast/common/crypto/certificate"
"chromiumos/tast/common/network/ping"
"chromiumos/tast/common/shillconst"
"chromiumos/tast/common/wifi/security"
"chromiumos/tast/common/wifi/security/wpa"
"chromiumos/tast/common/wifi/security/wpaeap"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/remote/wificell"
"chromiumos/tast/remote/wificell/hostapd"
"chromiumos/tast/services/cros/wifi"
"chromiumos/tast/testing"
"chromiumos/tast/timing"
)
func init() {
testing.AddTest(&testing.Test{
Func: MTBF,
Desc: "Run typical WiFi use cases and measure the Mean Time Between Failures (MTBF)",
Contacts: []string{
"chharry@google.com", // Test author
"chromeos-wifi-champs@google.com", // WiFi oncall rotation; or http://b/new?component=893827
},
Attr: []string{"group:wificell", "wificell_mtbf", "wificell_unstable"},
ServiceDeps: []string{wificell.TFServiceName},
Fixture: "wificellFixt",
Timeout: 5 * time.Hour,
})
}
func MTBF(ctx context.Context, s *testing.State) {
tf := s.FixtValue().(*wificell.TestFixture)
const (
pingDuration = 5 * time.Minute
pingInterval = 0.5 // seconds
// TODO(b/184596754): Find a way to suspend for longer time.
suspendDuration = 2 * time.Minute
simpleConnectTimeout = pingDuration + time.Minute
suspendReconnectTimeout = suspendDuration + pingDuration + time.Minute
suspendMoveTimeout = suspendDuration + pingDuration + time.Minute
suspendMoveBackTimeout = suspendReconnectTimeout
runOnceTimeout = simpleConnectTimeout + suspendReconnectTimeout + suspendMoveTimeout + suspendMoveBackTimeout +
time.Minute // Misc configuring and cleanup
waitSuspendTimeout = 10 * time.Second
timeForReinit = time.Second * 10
ap1SSIDPrefix = "TAST_WIFI_MTBF_AP1_"
ap2SSIDPrefix = "TAST_WIFI_MTBF_AP2_"
)
eapCert1 := certificate.TestCert1()
ap1Config := func() ([]hostapd.Option, security.ConfigFactory) {
mac, err := hostapd.RandomMAC()
if err != nil {
s.Fatal("Failed to generate a random MAC address: ", err)
}
ssid := hostapd.RandomSSID(ap1SSIDPrefix)
opts := []hostapd.Option{
hostapd.SSID(ssid), hostapd.BSSID(mac.String()),
hostapd.Mode(hostapd.Mode80211nPure), hostapd.Channel(1),
hostapd.HTCaps(hostapd.HTCapHT20),
}
secConfFac := wpa.NewConfigFactory("chromeos", wpa.Mode(wpa.ModePureWPA2), wpa.Ciphers2(wpa.CipherCCMP))
return opts, secConfFac
}
ap2Config := func() ([]hostapd.Option, security.ConfigFactory) {
mac, err := hostapd.RandomMAC()
if err != nil {
s.Fatal("Failed to generate a random MAC address: ", err)
}
ssid := hostapd.RandomSSID(ap2SSIDPrefix)
opts := []hostapd.Option{
hostapd.SSID(ssid), hostapd.BSSID(mac.String()),
hostapd.Mode(hostapd.Mode80211acPure), hostapd.Channel(157),
hostapd.HTCaps(hostapd.HTCapHT40Plus),
hostapd.VHTCaps(hostapd.VHTCapSGI80), hostapd.VHTChWidth(hostapd.VHTChWidth80), hostapd.VHTCenterChannel(155),
}
secConfFac := wpaeap.NewConfigFactory(
eapCert1.CACred.Cert, eapCert1.ServerCred,
wpaeap.ClientCACert(eapCert1.CACred.Cert),
wpaeap.ClientCred(eapCert1.ClientCred),
)
return opts, secConfFac
}
// TODO(b/184599948): Determine the traffic type and pass/fail criteria.
traffic := func(ctx context.Context, ip net.IP) error {
s.Log("Start pinging for ", pingDuration)
return tf.PingFromDUT(ctx, ip.String(),
ping.Count(int(pingDuration.Seconds()/pingInterval)),
ping.Interval(pingInterval),
)
}
configureAP := func(ctx context.Context, opts []hostapd.Option, secConfFac security.ConfigFactory) *wificell.APIface {
ap, err := tf.ConfigureAP(ctx, opts, secConfFac)
if err != nil {
s.Fatal("Failed to start the AP: ", err)
}
return ap
}
deconfigAP := func(ctx context.Context, ap **wificell.APIface) {
if *ap != nil {
if err := tf.DeconfigAP(ctx, *ap); err != nil {
s.Fatal("Failed to deconfigure the AP: ", err)
}
*ap = nil
}
}
bgSuspend := func(ctx context.Context, duration time.Duration) (suspendErrCh <-chan error, cleanup func()) {
ch := make(chan error, 1)
bgCtx, cancel := context.WithCancel(ctx)
go func() {
defer close(ch)
ch <- tf.WifiClient().Suspend(bgCtx, duration)
}()
return ch, func() {
// In case we failed and returned early in fg routine, cancel the bg routine and wait for it.
cancel()
<-ch
}
}
// Vars that record the iteration info of the MTBF test and will also be reset/updated in runOnce.
var (
run = 1
step = 1
)
runStep := func(ctx context.Context, desc string, stepTimeout time.Duration, f func(ctx context.Context) error) (succeeded bool) {
defer func() { step++ }()
ctx, cancel := context.WithTimeout(ctx, stepTimeout)
defer cancel()
s.Logf("Run %d: Step %d: %v", run, step, desc)
ctx, st := timing.Start(ctx, desc)
defer st.End()
if err := f(ctx); err != nil {
s.Logf("Failed at run %d: step %d: %v, err=%v", run, step, desc, err)
return false
}
return true
}
runOnce := func(ctx context.Context) (succeeded bool) {
defer func() {
run++
step = 1
}()
ap1Opts, ap1SecConfFac := ap1Config()
ap2Opts, ap2SecConfFac := ap2Config()
// Reinit the DUT after each round.
defer func(ctx context.Context) {
if err := tf.Reinit(ctx); err != nil {
s.Log("Failed to reinit the DUT: ", err)
succeeded = false
}
}(ctx)
ctx, cancel := ctxutil.Shorten(ctx, timeForReinit)
defer cancel()
ap1 := configureAP(ctx, ap1Opts, ap1SecConfFac)
defer deconfigAP(ctx, &ap1)
ctx, cancel = tf.ReserveForDeconfigAP(ctx, ap1)
defer cancel()
// Schedule defer for AP2 before configuring to make sure
// the AP will be cleaned up after each round.
var ap2 *wificell.APIface
defer deconfigAP(ctx, &ap2)
// Borrow the reserve of AP1 as the AP2 is not yet configured.
ctx, cancel = tf.ReserveForDeconfigAP(ctx, ap1)
defer cancel()
var ap1ServicePath string
if !runStep(ctx, "Connect to AP1 and perform ping test", simpleConnectTimeout, func(ctx context.Context) error {
ap1Conn, err := tf.ConnectWifiAP(ctx, ap1)
ap1ServicePath = ap1Conn.ServicePath
if err != nil {
return errors.Wrap(err, "failed to connect to AP1")
}
if err := traffic(ctx, ap1.ServerIP()); err != nil {
return errors.Wrap(err, "failed to generate traffics to AP1")
}
return nil
}) {
return false
}
if !runStep(ctx, fmt.Sprintf("Suspend for %v and reconnect to AP1", suspendDuration), suspendReconnectTimeout, func(ctx context.Context) error {
if _, err := tf.WifiClient().SuspendAssertConnect(ctx, suspendDuration); err != nil {
return errors.Wrap(err, "failed to suspend and reconnect to AP1")
}
if err := traffic(ctx, ap1.ServerIP()); err != nil {
return errors.Wrap(err, "failed to generate traffics to AP1")
}
return nil
}) {
return false
}
if !runStep(ctx, fmt.Sprintf("Suspend for %v and move to AP2", suspendDuration), suspendMoveTimeout, func(ctx context.Context) error {
disconnFromAp1Props, err := tf.WifiClient().ExpectShillProperty(ctx, ap1ServicePath, []*wificell.ShillProperty{{
Property: shillconst.ServicePropertyState,
ExpectedValues: []interface{}{shillconst.ServiceStateIdle},
Method: wifi.ExpectShillPropertyRequest_ON_CHANGE,
}}, nil)
if err != nil {
return errors.Wrap(err, "failed to start shill property watcher for disconnect from AP1")
}
suspendErrCh, cleanup := bgSuspend(ctx, suspendDuration)
defer cleanup()
// Wait for the DUT to fall asleep before changing AP config.
susCtx, cancel := context.WithTimeout(ctx, waitSuspendTimeout)
defer cancel()
if err := s.DUT().WaitUnreachable(susCtx); err != nil {
return errors.Wrap(err, "failed to wait for the DUT become unreachable")
}
deconfigAP(ctx, &ap1)
ap2 = configureAP(ctx, ap2Opts, ap2SecConfFac)
s.Log("AP setup done; Waiting for the suspend routine")
if err := <-suspendErrCh; err != nil {
return errors.Wrap(err, "failed to suspend")
}
if _, err := disconnFromAp1Props(); err != nil {
return errors.Wrap(err, "failed to disconnect from AP1")
}
if _, err := tf.ConnectWifiAP(ctx, ap2); err != nil {
return errors.Wrap(err, "failed to connect to AP2")
}
if err := traffic(ctx, ap2.ServerIP()); err != nil {
return errors.Wrap(err, "failed to generate traffics to AP2")
}
return nil
}) {
return false
}
return runStep(ctx, fmt.Sprintf("Suspend for %v and move back to AP1", suspendDuration), suspendMoveBackTimeout, func(ctx context.Context) error {
backToAP1Props, err := tf.WifiClient().ExpectShillProperty(ctx, ap1ServicePath, []*wificell.ShillProperty{{
Property: shillconst.ServicePropertyState,
ExpectedValues: []interface{}{shillconst.ServiceStateIdle},
Method: wifi.ExpectShillPropertyRequest_CHECK_ONLY,
}, {
Property: shillconst.ServicePropertyIsConnected,
ExpectedValues: []interface{}{true},
Method: wifi.ExpectShillPropertyRequest_ON_CHANGE,
}}, nil)
if err != nil {
return errors.Wrap(err, "failed to start shill property watcher for reconnect to AP1")
}
suspendErrCh, cleanup := bgSuspend(ctx, suspendDuration)
defer cleanup()
// Wait for the DUT to fall asleep before changing AP config.
susCtx, cancel := context.WithTimeout(ctx, waitSuspendTimeout)
defer cancel()
if err := s.DUT().WaitUnreachable(susCtx); err != nil {
return errors.Wrap(err, "failed to wait for the DUT become unreachable")
}
deconfigAP(ctx, &ap2)
ap1 = configureAP(ctx, ap1Opts, ap1SecConfFac)
s.Log("AP setup done; Waiting for the suspend routine")
if err := <-suspendErrCh; err != nil {
return errors.Wrap(err, "failed to suspend")
}
if _, err := backToAP1Props(); err != nil {
return errors.Wrap(err, "failed to reconnect to AP1")
}
if err := traffic(ctx, ap1.ServerIP()); err != nil {
return errors.Wrap(err, "failed to generate traffics to AP1")
}
return nil
})
}
startTS := time.Now()
for {
runOnceCtx, cancel := context.WithTimeout(ctx, runOnceTimeout)
defer cancel()
succeeded := runOnce(runOnceCtx)
mtbf := time.Now().Sub(startTS)
if !succeeded {
s.Log("MTBF failed, mean time=", mtbf)
return
}
d, ok := ctx.Deadline()
if !ok {
s.Fatal("No deadline is set to the context")
}
if time.Now().Add(runOnceTimeout).After(d) {
// No enough time for the next round.
s.Log("MTBF passed, mean time=", mtbf)
return
}
}
}