Refactor TestE2E to work correctly in parallel mode

The TestE2E function is run on every Ginkgo parallel node.
The cluster cleanup, sanity checking, and log dump operations are only
intended to be called once, however, so these have been moved out into
SynchronizedBeforeSuite and SynchronizedAfterSuite blocks.
This commit is contained in:
Jeff Grafton 2016-01-22 18:16:41 -08:00
parent 458eb5284a
commit 4cc7f9d3f7

View File

@ -93,22 +93,13 @@ func init() {
flag.StringVar(&testContext.OutputPrintType, "output-print-type", "hr", "Comma separated list: 'hr' for human readable summaries 'json' for JSON ones.") flag.StringVar(&testContext.OutputPrintType, "output-print-type", "hr", "Comma separated list: 'hr' for human readable summaries 'json' for JSON ones.")
} }
func TestE2E(t *testing.T) { // setupProviderConfig validates and sets up cloudConfig based on testContext.Provider.
util.ReallyCrash = true func setupProviderConfig() error {
util.InitLogs() switch testContext.Provider {
defer util.FlushLogs() case "":
if testContext.ReportDir != "" {
if err := os.MkdirAll(testContext.ReportDir, 0755); err != nil {
glog.Errorf("Failed creating report directory: %v", err)
}
defer CoreDump(testContext.ReportDir)
}
if testContext.Provider == "" {
glog.Info("The --provider flag is not set. Treating as a conformance test. Some tests may not be run.") glog.Info("The --provider flag is not set. Treating as a conformance test. Some tests may not be run.")
}
if testContext.Provider == "gce" || testContext.Provider == "gke" { case "gce", "gke":
var err error var err error
Logf("Fetching cloud provider for %q\r\n", testContext.Provider) Logf("Fetching cloud provider for %q\r\n", testContext.Provider)
var tokenSource oauth2.TokenSource var tokenSource oauth2.TokenSource
@ -121,57 +112,63 @@ func TestE2E(t *testing.T) {
zone := testContext.CloudConfig.Zone zone := testContext.CloudConfig.Zone
region, err := gcecloud.GetGCERegion(zone) region, err := gcecloud.GetGCERegion(zone)
if err != nil { if err != nil {
glog.Fatalf("error parsing GCE region from zone %q: %v", zone, err) return fmt.Errorf("error parsing GCE/GKE region from zone %q: %v", zone, err)
} }
managedZones := []string{zone} // Only single-zone for now managedZones := []string{zone} // Only single-zone for now
cloudConfig.Provider, err = gcecloud.CreateGCECloud(testContext.CloudConfig.ProjectID, region, zone, managedZones, "" /* networkUrl */, tokenSource, false /* useMetadataServer */) cloudConfig.Provider, err = gcecloud.CreateGCECloud(testContext.CloudConfig.ProjectID, region, zone, managedZones, "" /* networkUrl */, tokenSource, false /* useMetadataServer */)
if err != nil { if err != nil {
glog.Fatal("Error building GCE provider: ", err) return fmt.Errorf("Error building GCE/GKE provider: ", err)
} }
} case "aws":
if testContext.Provider == "aws" {
awsConfig := "[Global]\n" awsConfig := "[Global]\n"
if cloudConfig.Zone == "" { if cloudConfig.Zone == "" {
glog.Fatal("gce-zone must be specified for AWS") return fmt.Errorf("gce-zone must be specified for AWS")
} }
awsConfig += fmt.Sprintf("Zone=%s\n", cloudConfig.Zone) awsConfig += fmt.Sprintf("Zone=%s\n", cloudConfig.Zone)
if cloudConfig.ClusterTag == "" { if cloudConfig.ClusterTag == "" {
glog.Fatal("--cluster-tag must be specified for AWS") return fmt.Errorf("--cluster-tag must be specified for AWS")
} }
awsConfig += fmt.Sprintf("KubernetesClusterTag=%s\n", cloudConfig.ClusterTag) awsConfig += fmt.Sprintf("KubernetesClusterTag=%s\n", cloudConfig.ClusterTag)
var err error var err error
cloudConfig.Provider, err = cloudprovider.GetCloudProvider(testContext.Provider, strings.NewReader(awsConfig)) cloudConfig.Provider, err = cloudprovider.GetCloudProvider(testContext.Provider, strings.NewReader(awsConfig))
if err != nil { if err != nil {
glog.Fatal("Error building AWS provider: ", err) return fmt.Errorf("Error building AWS provider: ", err)
} }
} }
// Disable skipped tests unless they are explicitly requested. return nil
if config.GinkgoConfig.FocusString == "" && config.GinkgoConfig.SkipString == "" { }
// TODO(ihmccreery) Remove [Skipped] once all [Skipped] labels have been reclassified.
config.GinkgoConfig.SkipString = `\[Flaky\]|\[Skipped\]|\[Feature:.+\]`
}
gomega.RegisterFailHandler(ginkgo.Fail)
c, err := loadClient() // There are certain operations we only want to run once per overall test invocation
if err != nil { // (such as deleting old namespaces, or verifying that all system pods are running.
glog.Fatal("Error loading client: ", err) // Because of the way Ginkgo runs tests in parallel, we must use SynchronizedBeforeSuite
} // to ensure that these operations only run on the first parallel Ginkgo node.
//
// This function takes two parameters: one function which runs on only the first Ginkgo node,
// returning an opaque byte array, and then a second function which runs on all Ginkgo nodes,
// accepting the byte array.
var _ = ginkgo.SynchronizedBeforeSuite(func() []byte {
// Run only on Ginkgo node 1
// Delete any namespaces except default and kube-system. This ensures no // Delete any namespaces except default and kube-system. This ensures no
// lingering resources are left over from a previous test run. // lingering resources are left over from a previous test run.
if testContext.CleanStart { if testContext.CleanStart {
c, err := loadClient()
if err != nil {
glog.Fatal("Error loading client: ", err)
}
deleted, err := deleteNamespaces(c, nil /* deleteFilter */, []string{api.NamespaceSystem, api.NamespaceDefault}) deleted, err := deleteNamespaces(c, nil /* deleteFilter */, []string{api.NamespaceSystem, api.NamespaceDefault})
if err != nil { if err != nil {
t.Errorf("Error deleting orphaned namespaces: %v", err) Failf("Error deleting orphaned namespaces: %v", err)
} }
glog.Infof("Waiting for deletion of the following namespaces: %v", deleted) glog.Infof("Waiting for deletion of the following namespaces: %v", deleted)
if err := waitForNamespacesDeleted(c, deleted, namespaceCleanupTimeout); err != nil { if err := waitForNamespacesDeleted(c, deleted, namespaceCleanupTimeout); err != nil {
glog.Fatalf("Failed to delete orphaned namespaces %v: %v", deleted, err) Failf("Failed to delete orphaned namespaces %v: %v", deleted, err)
} }
} }
@ -180,14 +177,63 @@ func TestE2E(t *testing.T) {
// test pods from running, and tests that ensure all pods are running and // test pods from running, and tests that ensure all pods are running and
// ready will fail). // ready will fail).
if err := waitForPodsRunningReady(api.NamespaceSystem, testContext.MinStartupPods, podStartupTimeout); err != nil { if err := waitForPodsRunningReady(api.NamespaceSystem, testContext.MinStartupPods, podStartupTimeout); err != nil {
t.Errorf("Error waiting for all pods to be running and ready: %v", err) Failf("Error waiting for all pods to be running and ready: %v", err)
return
} }
return nil
}, func(data []byte) {
// Run on all Ginkgo nodes
})
// Similar to SynchornizedBeforeSuite, we want to run some operations only once (such as collecting cluster logs).
// Here, the order of functions is reversed; first, the function which runs everywhere,
// and then the function that only runs on the first Ginkgo node.
var _ = ginkgo.SynchronizedAfterSuite(func() {
// Run on all Ginkgo nodes
}, func() {
// Run only Ginkgo on node 1
if testContext.ReportDir != "" {
CoreDump(testContext.ReportDir)
}
})
// TestE2E checks configuration parameters (specified through flags) and then runs
// E2E tests using the Ginkgo runner.
// If a "report directory" is specified, one or more JUnit test reports will be
// generated in this directory, and cluster logs will also be saved.
// This function is called on each Ginkgo node in parallel mode.
func TestE2E(t *testing.T) {
util.ReallyCrash = true
util.InitLogs()
defer util.FlushLogs()
// We must call setupProviderConfig first since SynchronizedBeforeSuite needs
// cloudConfig to be set up already.
if err := setupProviderConfig(); err != nil {
glog.Fatalf(err.Error())
}
gomega.RegisterFailHandler(ginkgo.Fail)
// Disable skipped tests unless they are explicitly requested.
if config.GinkgoConfig.FocusString == "" && config.GinkgoConfig.SkipString == "" {
// TODO(ihmccreery) Remove [Skipped] once all [Skipped] labels have been reclassified.
config.GinkgoConfig.SkipString = `\[Flaky\]|\[Skipped\]|\[Feature:.+\]`
}
// Run tests through the Ginkgo runner with output to console + JUnit for Jenkins // Run tests through the Ginkgo runner with output to console + JUnit for Jenkins
var r []ginkgo.Reporter var r []ginkgo.Reporter
if testContext.ReportDir != "" { if testContext.ReportDir != "" {
r = append(r, reporters.NewJUnitReporter(path.Join(testContext.ReportDir, fmt.Sprintf("junit_%02d.xml", config.GinkgoConfig.ParallelNode)))) // TODO: we should probably only be trying to create this directory once
// rather than once-per-Ginkgo-node.
if err := os.MkdirAll(testContext.ReportDir, 0755); err != nil {
glog.Errorf("Failed creating report directory: %v", err)
} else {
r = append(r, reporters.NewJUnitReporter(path.Join(testContext.ReportDir, fmt.Sprintf("junit_%02d.xml", config.GinkgoConfig.ParallelNode))))
}
} }
glog.Infof("Starting e2e run; %q", runId) glog.Infof("Starting e2e run %q on Ginkgo node %d", runId, config.GinkgoConfig.ParallelNode)
ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "Kubernetes e2e suite", r) ginkgo.RunSpecsWithDefaultAndCustomReporters(t, "Kubernetes e2e suite", r)
} }