2020-05-30 02:03:03 +00:00
|
|
|
package main
|
|
|
|
|
2020-05-30 03:44:20 +00:00
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"sync"
|
|
|
|
)
|
|
|
|
|
|
|
|
var sem chan Empty // semaphore to limit requess in flight
|
|
|
|
|
2020-05-30 02:03:03 +00:00
|
|
|
// 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
|
2020-05-30 20:10:56 +00:00
|
|
|
// This function makes use of concurrent requests to speed up testing.
|
2020-05-30 04:32:26 +00:00
|
|
|
func test(redirects []Redirect) error {
|
2020-05-30 02:03:03 +00:00
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// This string will hold output for any failed tests. It's displayed when all tests have completed
|
2020-05-30 03:44:20 +00:00
|
|
|
var summaryOutput string
|
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// 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
|
2020-05-30 03:44:20 +00:00
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// Loop through all redirects and queue them up
|
2020-05-30 03:44:20 +00:00
|
|
|
for _, redirect := range redirects {
|
2020-05-30 20:10:56 +00:00
|
|
|
wg.Add(1) // Add 1 resource to waitgroup
|
|
|
|
P(1) // Take 1 resource from semaphore
|
2020-05-30 03:44:20 +00:00
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// This anonymous function executes in a separate go routine and can run concurrently
|
2020-05-30 03:44:20 +00:00
|
|
|
go func(redirect Redirect) {
|
2020-05-30 20:10:56 +00:00
|
|
|
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
|
2020-05-30 03:44:20 +00:00
|
|
|
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 {
|
2020-05-30 20:10:56 +00:00
|
|
|
// Modifying summaryOutput is critical section
|
2020-05-30 03:44:20 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// Parse response location URL from header into URL object
|
2020-05-30 03:44:20 +00:00
|
|
|
destURL, err := resp.Location()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to parse response location to URL: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// Check that the redirect went to the correct location
|
2020-05-30 03:44:20 +00:00
|
|
|
if destURL.String() != redirect.destinationURL {
|
2020-05-30 20:10:56 +00:00
|
|
|
// Modifying summyarOutput is critical section
|
2020-05-30 03:44:20 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// Wait for all tests to complete
|
2020-05-30 03:44:20 +00:00
|
|
|
wg.Wait()
|
2020-05-30 04:03:16 +00:00
|
|
|
fmt.Printf("\ndone tests.\n---------------------------------------------\n")
|
|
|
|
|
2020-05-30 20:10:56 +00:00
|
|
|
// Display summaryOutput if any tests failed
|
2020-05-30 04:03:16 +00:00
|
|
|
if len(summaryOutput) > 0 {
|
|
|
|
fmt.Printf("Summary:\n\n%s", summaryOutput)
|
|
|
|
} else {
|
|
|
|
fmt.Println("All redirect tests succeeded.")
|
|
|
|
}
|
2020-05-30 03:44:20 +00:00
|
|
|
|
2020-05-30 02:03:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
2020-05-30 03:44:20 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|