mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
same fixed campaign as implementation-sweep, over a corpus that needs no build. each implementation gets its own port: the corpus holds pairs that write the same localStorage key, and one shared origin is one stored record shared between them.
179 lines
4.7 KiB
Go
179 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
readinessTimeout = 5 * time.Second
|
|
readinessInterval = 100 * time.Millisecond
|
|
)
|
|
|
|
// stubScriptPath is served from every implementation's own origin in place of a
|
|
// dependency that no longer answers.
|
|
const stubScriptPath = "/__corpus-sweep__/stub.js"
|
|
|
|
// deadDependency is polyfill.io, which was shut down after this corpus was
|
|
// pinned. One implementation loads it from its index.html; the request fails
|
|
// and the application works regardless, but it fails slowly, once per run, on
|
|
// every run. The corpus is served from this process, so the reference is
|
|
// rewritten to a local no-op on the way out.
|
|
var deadDependency = regexp.MustCompile(`https?://polyfill\.io/[^"'\s>]*`)
|
|
|
|
// staticServer serves the whole corpus tree on one implementation's port. The
|
|
// tree rather than the implementation's own directory, because an example that
|
|
// asks for a path above itself has to resolve the same way it would in the
|
|
// upstream repository. Only one implementation is ever visited on this port, so
|
|
// the origin still belongs to it alone.
|
|
type staticServer struct {
|
|
implementation implementation
|
|
listener net.Listener
|
|
server *http.Server
|
|
}
|
|
|
|
func startStaticServer(
|
|
corpusRoot string,
|
|
target implementation,
|
|
) (*staticServer, error) {
|
|
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", target.Port))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("serve %s: %w", target.Name, err)
|
|
}
|
|
server := &http.Server{Handler: corpusHandler(corpusRoot)}
|
|
running := &staticServer{
|
|
implementation: target,
|
|
listener: listener,
|
|
server: server,
|
|
}
|
|
go server.Serve(listener)
|
|
return running, nil
|
|
}
|
|
|
|
func (s *staticServer) stop() {
|
|
s.server.Close()
|
|
}
|
|
|
|
// waitReady confirms the served document answers before any run drives it. A
|
|
// wrong document path would otherwise reach the driver as a 404 page, and a
|
|
// sweep of 404 pages produces clean runs for every implementation.
|
|
//
|
|
// Only a transport error is retried. The listener is bound before the sweep
|
|
// starts, so a status that is not 200 is the server's answer about this
|
|
// document and waiting will not change it.
|
|
func (s *staticServer) waitReady(ctx context.Context) error {
|
|
url := s.implementation.URL()
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
deadline := time.Now().Add(readinessTimeout)
|
|
var lastErr error
|
|
for {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
response, err := client.Get(url)
|
|
if err == nil {
|
|
io.Copy(io.Discard, response.Body)
|
|
response.Body.Close()
|
|
if response.StatusCode == http.StatusOK {
|
|
return nil
|
|
}
|
|
return fmt.Errorf(
|
|
"%s answered %d, not the document",
|
|
url,
|
|
response.StatusCode,
|
|
)
|
|
}
|
|
lastErr = err
|
|
if time.Now().After(deadline) {
|
|
return fmt.Errorf(
|
|
"%s did not answer within %s: %w",
|
|
url,
|
|
readinessTimeout,
|
|
lastErr,
|
|
)
|
|
}
|
|
time.Sleep(readinessInterval)
|
|
}
|
|
}
|
|
|
|
func corpusHandler(corpusRoot string) http.Handler {
|
|
files := http.FileServer(http.Dir(corpusRoot))
|
|
return http.HandlerFunc(
|
|
func(writer http.ResponseWriter, request *http.Request) {
|
|
cleaned := path.Clean(
|
|
"/" + strings.TrimPrefix(request.URL.Path, "/"),
|
|
)
|
|
if cleaned == stubScriptPath {
|
|
writer.Header().Set("Content-Type", "application/javascript")
|
|
io.WriteString(
|
|
writer,
|
|
"/* corpus-sweep: dependency removed upstream */\n",
|
|
)
|
|
return
|
|
}
|
|
if !strings.HasSuffix(cleaned, ".html") {
|
|
files.ServeHTTP(writer, request)
|
|
return
|
|
}
|
|
body, modified, err := readDocument(corpusRoot, cleaned)
|
|
if err != nil {
|
|
// Never the file server's fallback: it answers a document it
|
|
// cannot read with a 200 directory listing, and a run driven at a
|
|
// listing explores nothing and comes back clean.
|
|
http.Error(writer, err.Error(), documentStatus(err))
|
|
return
|
|
}
|
|
rewritten := deadDependency.ReplaceAll(body, []byte(stubScriptPath))
|
|
http.ServeContent(
|
|
writer,
|
|
request,
|
|
path.Base(cleaned),
|
|
modified,
|
|
bytes.NewReader(rewritten),
|
|
)
|
|
},
|
|
)
|
|
}
|
|
|
|
func documentStatus(err error) int {
|
|
switch {
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
return http.StatusNotFound
|
|
case errors.Is(err, fs.ErrPermission):
|
|
return http.StatusForbidden
|
|
default:
|
|
return http.StatusInternalServerError
|
|
}
|
|
}
|
|
|
|
func readDocument(corpusRoot, cleaned string) ([]byte, time.Time, error) {
|
|
file, err := http.Dir(corpusRoot).Open(cleaned)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil || info.IsDir() {
|
|
return nil, time.Time{}, fmt.Errorf(
|
|
"%s is not a document",
|
|
filepath.FromSlash(cleaned),
|
|
)
|
|
}
|
|
body, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, time.Time{}, err
|
|
}
|
|
return body, info.ModTime(), nil
|
|
}
|