mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
feat(corpus-sweep): run one specification against a served corpus of implementations
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.
This commit is contained in:
1 parent
146152a3af
commit
341d6a0614
9 files changed
+2103
No files matched your search
@@ -0,0 +1,178 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user