package main import ( "fmt" "log" "net/http" "sync" ) var sem chan Empty // semaphore to limit requess in flight // test accepts a slice of type Redirect and returns an error // it performs actual HTTP GET requests on each source URL and validates that a redirect occurs // to the destination URL and that the redirect type/status code is correct // This function makes use of concurrent requests to speed up testing. func test(redirects []Redirect) error { // This string will hold output for any failed tests. It's displayed when all tests have completed var summaryOutput string // Set up some tools to handle concurrency wg := &sync.WaitGroup{} // Used to wait for all tests to finish mu := &sync.Mutex{} // Used to lock shared memory in critical sections sem = make(Semaphore, *maxConcurrentRequests) // Used to limit resources while having all requests queued up // Loop through all redirects and queue them up for _, redirect := range redirects { wg.Add(1) // Add 1 resource to waitgroup P(1) // Take 1 resource from semaphore // This anonymous function executes in a separate go routine and can run concurrently go func(redirect Redirect) { defer V(1) // If function exits (error or otherwise), put 1 resource back into semaphore defer wg.Done() // If function exits (error or otherwise), subtract 1 resource from waitgroup // Create an HTTP client and override CheckRedirect to return the last response error so we can check the redirect type client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } // writing to std out is critical section mu.Lock() fmt.Printf("Checking redirect for: %s\n", redirect.sourceURL) mu.Unlock() // Make the request resp, err := client.Get(redirect.sourceURL) if err != nil { log.Printf("HTTP GET failed for source URL '%s': %v", redirect.sourceURL, err) } // Check the status code if resp.StatusCode != redirect.statusCode { // Modifying summaryOutput is critical section mu.Lock() summaryOutput += fmt.Sprintf("redirect for source URL '%s': expected status code'%d': got '%d\n", redirect.sourceURL, redirect.statusCode, resp.StatusCode) mu.Unlock() return } // Parse response location URL from header into URL object destURL, err := resp.Location() if err != nil { log.Printf("failed to parse response location to URL: %v", err) } // Check that the redirect went to the correct location if destURL.String() != redirect.destinationURL { // Modifying summyarOutput is critical section mu.Lock() summaryOutput += fmt.Sprintf("redirect for source URL '%s': expected '%s': got '%s\n", redirect.sourceURL, redirect.destinationURL, destURL.String()) mu.Unlock() return } }(redirect) } // Wait for all tests to complete wg.Wait() fmt.Printf("\ndone tests.\n---------------------------------------------\n") // Display summaryOutput if any tests failed if len(summaryOutput) > 0 { fmt.Printf("Summary:\n\n%s", summaryOutput) } else { fmt.Println("All redirect tests succeeded.") } return nil } // Semaphore helper functions // Empty is an empty struct used by the semaphores type Empty struct{} // Semaphore is a channel which passes empty structs and acts as a resource lock type Semaphore chan Empty // P acquire n resources - standard semaphore design pattern to limit number of requests in flight func P(n int) { e := Empty{} for i := 0; i < n; i++ { sem <- e } } // V release n resources - standard semaphore design pattern to limit number of requests in flight func V(n int) { for i := 0; i < n; i++ { <-sem } }