HTTP Field Notes
Goal
Capture three real HTTP exchanges — a public site, a redirect, and your own server — then show what file:// lacks that all of them have. Report what your phone does with http://localhost:8000. Do not fix it. ---
Type: interactive-form
Unit: 3 — A second address
Goal
Capture three real HTTP exchanges — a public site, a redirect, and your own server — then show what file:// lacks that all of them have. Report what your phone does with http://localhost:8000. Do not fix it.
How this works
Run the commands, paste what comes back, do not tidy it. Fill starter/field-notes.md, then copy each block into starter/submission.json.
| Command | You get |
|---|---|
curl -v <url> | the whole conversation: > sent, < received |
curl -I <url> | status line and headers, no body |
Your tasks
- TODO A.
curl -v https://example.com. Paste the complete output. - TODO B.
curl -Ia URL that answers with a 3xx. Paste the head and the URL you used. - TODO C. Start
python3 -m http.server 8000in~/projects/first-siteif it is not already running.curl -I http://localhost:8000/index.html. Paste the complete head. - TODO D. The first line of TODO C, on its own.
- TODO E. The
Content-Typevalue from TODO C. Value only. - TODO F. Annotate at least six lines from A, B, or C, in this shape:
- LINE: `Server: SimpleHTTP/0.6 Python/3.14.6`
SOURCE: curl_i_local
WHAT IT IS: a response header
WHAT IT DOES: names the software that answered
- TODO G. What
file://has no room for, and whathttp://localhost:8000/index.htmlhas instead. At least 30 words. - TODO H. Type
http://localhost:8000on your phone. Write what happened, and what the server’s terminal did. At least 20 words. Report it. Do not fix it.
What the scaffolding is for
The local capture has to look like your server — HTTP/1.0 and a SimpleHTTP server header, not a generic HTTP/1.1 200 OK from a tutorial. TODO H is the wall Unit 4 exists to climb.
Expected output
TODO D looks like HTTP/1.0 200 OK. TODO E looks like text/html. TODO A still has its > and < markers.
Rules
- Paste captures. Do not retype them.
- Do not delete the
>/<markers. - Do not edit
tests/.
See rubric.md for how this is scored.
Steps
Your files
Download these onto your machine and fill them in. The brief says which, and what “done” looks like.
field-notes.md Download
# HTTP Field Notes — worksheet
Fill in every `TODO`. Then copy each block into the matching field of `submission.json`.
---
## READ ONLY — do not edit
### The artifact this course has been building
```
~/projects/first-site/index.html
```
You made that file with commands in Unit 1 and have opened it by double-clicking ever since, which
fills the address bar with:
```
file:///home/you/projects/first-site/index.html
```
Three slots in that URL are empty and cannot be filled: a host, a port, and a program listening.
This project is where that file gets a real URL instead of a location.
### The worked annotation — this is the standard
This is one entry, in the exact format the parser expects. Read what the WHAT IT DOES half is doing.
```
- LINE: `Server: SimpleHTTP/0.6 Python/3.14.6`
SOURCE: curl_i_local
WHAT IT IS: a response header, sent by the server, part of the head
WHAT IT DOES: names the software that answered me. It is how I can tell this response came from
my own machine and not from a site on the internet — and it quietly hands anyone who asks the
Python version I am running, which I did not choose to publish and cannot switch off.
```
Four rules the parser enforces on every entry:
1. `LINE:` is wrapped in backticks and appears **verbatim** somewhere in one of your three captures.
2. `SOURCE:` is one of `curl_v_public`, `curl_i_redirect`, `curl_i_local`.
3. `WHAT IT IS:` is not empty.
4. `WHAT IT DOES:` is at least 8 words.
The parser cannot tell whether your WHAT IT DOES half is any good. The rubric can. Do not restate
the line.
---
## TODO A — `curl_v_public`
Run `curl -v https://example.com` and paste **all** of the output below, `>` and `<` markers intact.
```
TODO A: paste here
```
## TODO B — `curl_i_redirect`
Find a URL that answers with a status code between `300` and `399`. Screen candidates fast:
```
curl -s -o /dev/null -w '%{http_code}\n' https://example.com
```
Then run `curl -I` against the one you found and paste the whole response head.
```
TODO B: paste here
```
**Which URL did you use?**
```
TODO B (url): write it here
```
## TODO C — `curl_i_local`
```
cd ~/projects/first-site
python3 -m http.server 8000
```
Leave it running. In a second terminal:
```
curl -I http://localhost:8000/index.html
```
Paste the whole response head.
```
TODO C: paste here
```
## TODO D — `status_line_local`
The first line of TODO C, on its own. Its grammar is `<protocol> <status-code> <reason-phrase>`.
```
TODO D: paste here
```
## TODO E — `content_type_local`
The **value** of the `Content-Type` header from TODO C. Value only — no header name, no colon, no
trailing spaces. Character for character.
```
TODO E: paste here
```
## TODO F — `annotations`
At least six entries, in the format of the READ ONLY worked example above. Every `LINE:` must appear
verbatim in TODO A, TODO B, or TODO C. Spread them across all three captures.
```
- LINE: `TODO F1`
SOURCE:
WHAT IT IS:
WHAT IT DOES:
- LINE: `TODO F2`
SOURCE:
WHAT IT IS:
WHAT IT DOES:
- LINE: `TODO F3`
SOURCE:
WHAT IT IS:
WHAT IT DOES:
- LINE: `TODO F4`
SOURCE:
WHAT IT IS:
WHAT IT DOES:
- LINE: `TODO F5`
SOURCE:
WHAT IT IS:
WHAT IT DOES:
- LINE: `TODO F6`
SOURCE:
WHAT IT IS:
WHAT IT DOES:
```
Lines worth considering, if you are stuck on where to look:
- the first `>` line of TODO A — the request line, three slots
- the `>` line beginning `Host:` — the one header HTTP/1.1 makes compulsory
- the first `<` line of TODO A — the status line, with the code in the middle slot
- the status line of TODO B — a first digit that is not `2`
- any line of TODO C you did not expect to be there
## TODO G — `file_url_comparison`
Double-click `index.html` so the address bar reads
`file:///home/you/projects/first-site/index.html`. Open DevTools, go to the **Network** panel,
reload, and look at the Status column and the response headers.
Then compare against your TODO C capture. Name **both** things `file://` does not have, name at
least one header from your own capture as the contrast, and say *why* — what would have had to
happen for a status code to exist at all.
```
TODO G: write here (at least 30 words)
```
## TODO H — `phone_result`
Do this last, after everything above is filled in and your server is still running.
Pick up your phone. Same Wi-Fi as the laptop. Type into its browser:
```
http://localhost:8000
```
Write down what actually happened: what the phone's screen showed, what the laptop showed at the
same URL, and whether a new line appeared in the terminal where your server is running.
Report it. Do not fix it.
```
TODO H: write here (at least 20 words)
```
submission.json Download
{
"_format": "HTTP Field Notes submission. Every value is a string. Paste raw capture text; do not clean it up. Delete nothing from this file except the TODO placeholders.",
"curl_v_public": "TODO A — the complete output of: curl -v https://example.com (keep every > and < line)",
"curl_i_redirect": "TODO B — the complete response head of: curl -I <a URL that answers 300-399>",
"curl_i_redirect_url": "TODO B — the URL you used",
"curl_i_local": "TODO C — the complete response head of: curl -I http://localhost:8000/index.html",
"status_line_local": "TODO D — the first line of curl_i_local, on its own",
"content_type_local": "TODO E — the value of the Content-Type header from curl_i_local. Value only.",
"annotations": "TODO F — at least six entries in this format, one blank line between entries:\n\n- LINE: `Server: SimpleHTTP/0.6 Python/3.14.6`\n SOURCE: curl_i_local\n WHAT IT IS: a response header, part of the head\n WHAT IT DOES: names the software that answered me, which is how I can tell this response came from my own machine\n",
"file_url_comparison": "TODO G — what the file:// load has no room for, what http://localhost:8000/index.html has instead, and why. At least 30 words.",
"phone_result": "TODO H — what happened when you typed http://localhost:8000 into your phone's browser on the same Wi-Fi, and what your server's terminal did. At least 20 words. Report it; do not fix it."
}
On your machine, run the tests in this project’s tests/ folder. Each one
prints PASS or a FAIL: line that names what is wrong.
How it is graded
| Criterion | What earns it | Weight |
|---|---|---|
| Three genuine, complete captures |
Deduct for: a capture that has been tidied, truncated mid-header, or reflowed; a | 30 |
| Annotations explain what each line does | At least six distinct lines, each verbatim from one of the three captures, each with a WHAT IT IS half naming the line's structural job (request line, request header, status line, response header) and a WHAT IT DOES half stating what changes because that line is present. Full credit requires that the WHAT IT DOES half is not a restatement of the line. Compare:
Full credit also requires spread: the six lines are not all response headers from one capture.
A set covering a request line, a request header, a status line, and headers from more than one
response scores above a set of six headers lifted from Deduct for: any annotated line that cannot be found in the submitted captures; two annotations that say the same thing about two lines; a WHAT IT DOES half that only re-labels the line ("this is the date", "this is the length"). | 25 |
The file:// comparison names both absences | The comparison states that loading Full credit requires the reason, not only the observation: nothing answered, so there is nothing to have a status. A status code is a server's verdict on a request; with no server there was no request, no verdict, and no headers describing a response that never existed. The browser read the disk itself. Deduct for: describing the difference as "fewer headers" or "the headers are hidden" rather than as
an absence of the whole category; claiming the | 25 |
| The phone result is reported as an observation | The learner actually tried Full credit requires the report to be honest and unresolved: what was typed, what was seen, and
what the server's log did or did not show. Stating what Deduct for: an empty or one-word entry; a guess written without trying it; a report that repeats the
| 20 |
| Total | 100 |
Test cases and grader source
| Test | Checks | Expected | Weight |
|---|---|---|---|
| local_response_authentic | Catches a shortcut: Verifies that curl_i_local is a response the learner's own python3 -m http.server actually produced, and not a generic response head pasted from a tutorial. The shortcut this catches: a learner who never got their server running (or never bothered) pastes the HTTP/1.1 200 OK block that every HTTP tutorial on the internet opens with. That block is wrong in two ways that cannot be faked by accident: * This server hard-codes protocol_version = "HTTP/1.0", so its status line is HTTP/1.0 200 OK. A pasted tutorial response says HTTP/1.1. * This server always identifies itself with Server: SimpleHTTP/0.6 Python/<version>. A tutorial response carries nginx, Apache, cloudflare, or no Server header at all. | PASS | 30 |
| content_type_real_file | Catches a shortcut: Verifies that content_type_local is exactly text/html, with nothing after it, and that curl_i_local contains no charset anywhere. The shortcut this catches is the subtle one, and it catches a whole missing project. python3 -m http.server sends Content-type: text/html — no charset — when it reads a real .html file off the disk. It sends Content-type: text/html; charset=utf-8 only for pages it generates itself: the directory listing, and the 404 page. So a submission whose Content-Type carries charset=utf-8 did not capture a served file. It captured a directory listing. And the server only generates a directory listing when it looked inside the folder for an index page — "the directory is checked for an index page as specified by index_pages", defaulting to ("index.html", "index.htm") — and did not find one. Which means the learner never created ~/projects/first-site/index.html: the file this entire course has been building, the file the server exists to serve. They started a server in an empty or wrong directory, pointed curl at /, got a page the server wrote on the spot, and pasted it. One header, seven characters long, proves the artifact is missing. | PASS | 25 |
| public_capture_both_halves | Verifies that curl_v_public is real curl -v output holding BOTH halves of one conversation: at least one line curl sent (marked >) and at least one line curl received (marked <), with a request line among the sent lines and a status line among the received lines. curl -v is the only instrument in this unit that shows both messages at once. A capture with only < lines is curl -I output pasted into the wrong field; a capture with the markers stripped is a capture that was edited. | PASS | 15 |
| status_line_local | Verifies that status_line_local is exactly HTTP/1.0 200 OK and that it is the first non-empty line of curl_i_local — that the learner copied the status line out of their own capture rather than writing down what they expected it to say. | PASS | 10 |
| redirect_is_3xx | Verifies that curl_i_redirect is a response head whose status line carries a code in the Redirection class, 300-399 — a third exchange whose first digit is neither 2 nor 4. The point of the field is that the learner had to go and find one. Screening candidates with curl -s -o /dev/null -w '%{http_code}\\n' <url> is the intended method; the result must be a real capture, so this test also rejects a status line that is present without any headers under it. | PASS | 10 |
| annotations_cover_six | Verifies that annotations holds at least six entries, that each one quotes a DISTINCT line, that every quoted line appears verbatim somewhere in the three submitted captures, and that each entry has a non-empty WHAT IT IS and a WHAT IT DOES of at least 8 words. This test cannot judge whether an annotation is any good — that is the rubric's craft criterion. It can prove the annotated lines were really captured, which is what stops six invented headers from scoring. | PASS | 10 |
tests/test_local_response_authentic.py
#!/usr/bin/env python3
"""
test_local_response_authentic — weight 30 — ADVERSARIAL
Verifies that `curl_i_local` is a response the learner's own `python3 -m http.server`
actually produced, and not a generic response head pasted from a tutorial.
The shortcut this catches: a learner who never got their server running (or never
bothered) pastes the `HTTP/1.1 200 OK` block that every HTTP tutorial on the internet
opens with. That block is wrong in two ways that cannot be faked by accident:
* This server hard-codes `protocol_version = "HTTP/1.0"`, so its status line is
`HTTP/1.0 200 OK`. A pasted tutorial response says `HTTP/1.1`.
* This server always identifies itself with `Server: SimpleHTTP/0.6 Python/<version>`.
A tutorial response carries nginx, Apache, cloudflare, or no Server header at all.
Prints PASS or a line beginning FAIL:.
"""
import json
import re
import sys
def load(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def main(path):
data = load(path)
raw = data.get("curl_i_local", "")
if not isinstance(raw, str) or not raw.strip():
print("FAIL: curl_i_local is empty. Run `curl -I http://localhost:8000/index.html` "
"against your own running server and paste the whole response head.")
return 1
if "TODO" in raw:
print("FAIL: curl_i_local still contains the TODO placeholder from the starter.")
return 1
lines = [line.strip() for line in raw.splitlines() if line.strip()]
status_ok = any(line.startswith("HTTP/1.0 200") for line in lines)
server_line = next((line for line in lines
if re.match(r"^Server:\s*SimpleHTTP/", line, re.IGNORECASE)), None)
if not status_ok:
wrong_version = next((line for line in lines if line.startswith("HTTP/")), None)
if wrong_version and wrong_version.startswith("HTTP/1.1"):
print("FAIL: curl_i_local's status line is '%s'. `python3 -m http.server` answers "
"HTTP/1.0 — it hard-codes protocol_version = \"HTTP/1.0\". This response did not "
"come from your server." % wrong_version)
elif wrong_version:
print("FAIL: curl_i_local's status line is '%s'. Expected a line beginning "
"'HTTP/1.0 200'." % wrong_version)
else:
print("FAIL: curl_i_local contains no status line at all. The first line of a "
"`curl -I` response is <protocol> <status-code> <reason-phrase>.")
return 1
if server_line is None:
present = next((line for line in lines
if line.lower().startswith("server:")), None)
if present:
print("FAIL: curl_i_local's Server header is '%s'. Your own server always sends "
"'Server: SimpleHTTP/0.6 Python/<version>'. This response came from some other "
"machine." % present)
else:
print("FAIL: curl_i_local has no 'Server: SimpleHTTP/...' header. "
"`python3 -m http.server` sends one on every response.")
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))
tests/test_content_type_real_file.py
#!/usr/bin/env python3
"""
test_content_type_real_file — weight 25 — ADVERSARIAL
Verifies that `content_type_local` is exactly `text/html`, with nothing after it, and
that `curl_i_local` contains no charset anywhere.
The shortcut this catches is the subtle one, and it catches a whole missing project.
`python3 -m http.server` sends `Content-type: text/html` — no charset — when it reads a
real `.html` file off the disk. It sends `Content-type: text/html; charset=utf-8` only
for pages it *generates* itself: the directory listing, and the 404 page.
So a submission whose Content-Type carries `charset=utf-8` did not capture a served
file. It captured a directory listing. And the server only generates a directory
listing when it looked inside the folder for an index page — "the directory is checked
for an index page as specified by `index_pages`", defaulting to `("index.html",
"index.htm")` — and did not find one.
Which means the learner never created `~/projects/first-site/index.html`: the file this
entire course has been building, the file the server exists to serve. They started a
server in an empty or wrong directory, pointed curl at `/`, got a page the server wrote
on the spot, and pasted it. One header, seven characters long, proves the artifact is
missing.
Prints PASS or a line beginning FAIL:.
"""
import json
import sys
def load(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def main(path):
data = load(path)
value = data.get("content_type_local", "")
capture = data.get("curl_i_local", "")
if not isinstance(value, str) or not value.strip():
print("FAIL: content_type_local is empty. Copy the value of the Content-Type header "
"out of your curl_i_local capture.")
return 1
if "TODO" in value:
print("FAIL: content_type_local still contains the TODO placeholder from the starter.")
return 1
normalised = value.strip().lower()
if normalised.startswith("content-type:"):
print("FAIL: content_type_local is '%s'. Copy the header's VALUE only — no header name, "
"no colon." % value.strip())
return 1
if "charset" in normalised:
print("FAIL: content_type_local is '%s'. A real .html file read off your disk gets "
"'text/html' with no charset. The charset appears only on pages this server "
"GENERATES — the directory listing and the 404 page. You captured a directory "
"listing, which means the server looked in the folder for index.html and did not "
"find one. Create ~/projects/first-site/index.html, then capture "
"`curl -I http://localhost:8000/index.html` again." % value.strip())
return 1
if normalised != "text/html":
print("FAIL: content_type_local is '%s'. Expected exactly 'text/html'." % value.strip())
return 1
if isinstance(capture, str) and "charset" in capture.lower():
print("FAIL: content_type_local says 'text/html' but curl_i_local contains a charset. "
"Those two cannot both have come from the same response. Recapture, and copy the "
"value out of the capture rather than typing it.")
return 1
if isinstance(capture, str) and "text/html" not in capture.lower():
print("FAIL: content_type_local says 'text/html' but no Content-Type header carrying "
"'text/html' appears in curl_i_local. Copy the value out of your own capture.")
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))
tests/test_public_capture_both_halves.py
#!/usr/bin/env python3
"""
test_public_capture_both_halves — weight 15
Verifies that `curl_v_public` is real `curl -v` output holding BOTH halves of one
conversation: at least one line curl sent (marked `>`) and at least one line curl
received (marked `<`), with a request line among the sent lines and a status line
among the received lines.
`curl -v` is the only instrument in this unit that shows both messages at once. A
capture with only `<` lines is `curl -I` output pasted into the wrong field; a capture
with the markers stripped is a capture that was edited.
Prints PASS or a line beginning FAIL:.
"""
import json
import re
import sys
METHODS = ("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
def load(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def main(path):
data = load(path)
raw = data.get("curl_v_public", "")
if not isinstance(raw, str) or not raw.strip():
print("FAIL: curl_v_public is empty. Run `curl -v https://example.com` and paste the "
"complete output.")
return 1
if "TODO" in raw:
print("FAIL: curl_v_public still contains the TODO placeholder from the starter.")
return 1
sent, received = [], []
for line in raw.splitlines():
stripped = line.strip()
if stripped.startswith(">"):
sent.append(stripped[1:].strip())
elif stripped.startswith("<"):
received.append(stripped[1:].strip())
if not sent and not received:
print("FAIL: curl_v_public contains no '>' lines and no '<' lines. Either -v was not "
"used, or the markers were removed when the output was cleaned up. Paste the "
"output exactly as curl printed it.")
return 1
if not sent:
print("FAIL: curl_v_public contains %d received ('<') lines but no sent ('>') lines. "
"The request half of the conversation is missing." % len(received))
return 1
if not received:
print("FAIL: curl_v_public contains %d sent ('>') lines but no received ('<') lines. "
"The response half of the conversation is missing." % len(sent))
return 1
if not any(line.split(" ")[0] in METHODS and line.endswith(("HTTP/1.0", "HTTP/1.1", "HTTP/2",
"HTTP/3"))
for line in sent if line):
print("FAIL: none of the '>' lines is a request line. A request line is three "
"space-separated slots: <method> <request-target> <protocol>, for example "
"'GET / HTTP/1.1'.")
return 1
if not any(re.match(r"^HTTP/[0-9.]+\s+[1-5][0-9]{2}", line) for line in received):
print("FAIL: none of the '<' lines is a status line. A status line is "
"<protocol> <status-code> <reason-phrase>, and the code is a three-digit number "
"in the middle slot.")
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))
tests/test_status_line_local.py
#!/usr/bin/env python3
"""
test_status_line_local — weight 10
Verifies that `status_line_local` is exactly `HTTP/1.0 200 OK` and that it is the first
non-empty line of `curl_i_local` — that the learner copied the status line out of their
own capture rather than writing down what they expected it to say.
Prints PASS or a line beginning FAIL:.
"""
import json
import sys
EXPECTED = "HTTP/1.0 200 OK"
def load(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def main(path):
data = load(path)
value = data.get("status_line_local", "")
capture = data.get("curl_i_local", "")
if not isinstance(value, str) or not value.strip():
print("FAIL: status_line_local is empty. Copy the first line of your curl_i_local "
"capture.")
return 1
if "TODO" in value:
print("FAIL: status_line_local still contains the TODO placeholder from the starter.")
return 1
normalised = " ".join(value.split())
if normalised != EXPECTED:
print("FAIL: status_line_local is '%s'. Expected '%s' — the three slots of a status line "
"are <protocol> <status-code> <reason-phrase>, and this server answers HTTP/1.0."
% (normalised, EXPECTED))
return 1
first = next((line.strip() for line in str(capture).splitlines() if line.strip()), "")
if " ".join(first.split()) != EXPECTED:
print("FAIL: status_line_local says '%s' but the first non-empty line of curl_i_local is "
"'%s'. Copy the status line out of your own capture." % (EXPECTED, first))
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))
tests/test_redirect_is_3xx.py
#!/usr/bin/env python3
"""
test_redirect_is_3xx — weight 10
Verifies that `curl_i_redirect` is a response head whose status line carries a code in
the Redirection class, 300-399 — a third exchange whose first digit is neither 2 nor 4.
The point of the field is that the learner had to go and find one. Screening candidates
with `curl -s -o /dev/null -w '%{http_code}\\n' <url>` is the intended method; the
result must be a real capture, so this test also rejects a status line that is present
without any headers under it.
Prints PASS or a line beginning FAIL:.
"""
import json
import re
import sys
CLASSES = {
1: "Informational (100-199) — not the final answer yet",
2: "Successful (200-299) — you asked, and you got it",
4: "Client error (400-499) — your request was wrong",
5: "Server error (500-599) — the server broke",
}
def load(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def main(path):
data = load(path)
raw = data.get("curl_i_redirect", "")
if not isinstance(raw, str) or not raw.strip():
print("FAIL: curl_i_redirect is empty. Find a URL that answers with a code between 300 "
"and 399 and paste the `curl -I` response head.")
return 1
if "TODO" in raw:
print("FAIL: curl_i_redirect still contains the TODO placeholder from the starter.")
return 1
lines = [line.strip() for line in raw.splitlines() if line.strip()]
status = next((line for line in lines if re.match(r"^HTTP/[0-9.]+\s", line)), None)
if status is None:
print("FAIL: curl_i_redirect has no status line. The first line of a `curl -I` response "
"begins with the protocol, for example 'HTTP/1.1 301 Moved Permanently'.")
return 1
match = re.match(r"^HTTP/[0-9.]+\s+([0-9]{3})", status)
if match is None:
print("FAIL: curl_i_redirect's status line is '%s' and carries no three-digit status "
"code in the middle slot." % status)
return 1
code = int(match.group(1))
if not 300 <= code <= 399:
family = CLASSES.get(code // 100, "an unrecognised class")
print("FAIL: curl_i_redirect's status code is %d, which is %s. This field wants a "
"Redirection code, 300-399. Screen candidate URLs with "
"`curl -s -o /dev/null -w '%%{http_code}\\n' <url>` until one prints a number "
"beginning with 3." % (code, family))
return 1
header_lines = [line for line in lines
if line is not status and re.match(r"^[A-Za-z][A-Za-z0-9-]*:\s*\S", line)]
if not header_lines:
print("FAIL: curl_i_redirect has a %d status line but no response headers under it. "
"Paste the whole response head, not just the first line." % code)
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))
tests/test_annotations_cover_six.py
#!/usr/bin/env python3
"""
test_annotations_cover_six — weight 10
Verifies that `annotations` holds at least six entries, that each one quotes a DISTINCT
line, that every quoted line appears verbatim somewhere in the three submitted captures,
and that each entry has a non-empty WHAT IT IS and a WHAT IT DOES of at least 8 words.
This test cannot judge whether an annotation is any good — that is the rubric's craft
criterion. It can prove the annotated lines were really captured, which is what stops
six invented headers from scoring.
Prints PASS or a line beginning FAIL:.
"""
import json
import re
import sys
MINIMUM = 6
MIN_DOES_WORDS = 8
CAPTURE_FIELDS = ("curl_v_public", "curl_i_redirect", "curl_i_local")
def load(path):
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
def parse(raw):
"""Split the annotations blob into entries keyed by LINE."""
entries = []
current = None
for line in raw.splitlines():
stripped = line.strip()
match = re.match(r"^-?\s*LINE:\s*`(.+?)`\s*$", stripped)
if match:
current = {"line": match.group(1), "is": "", "does": ""}
entries.append(current)
continue
if current is None:
continue
is_match = re.match(r"^WHAT IT IS:\s*(.*)$", stripped, re.IGNORECASE)
if is_match:
current["is"] = is_match.group(1).strip()
current["_last"] = "is"
continue
does_match = re.match(r"^WHAT IT DOES:\s*(.*)$", stripped, re.IGNORECASE)
if does_match:
current["does"] = does_match.group(1).strip()
current["_last"] = "does"
continue
if re.match(r"^SOURCE:", stripped, re.IGNORECASE):
current["_last"] = None
continue
if stripped and current.get("_last"):
current[current["_last"]] = (current[current["_last"]] + " " + stripped).strip()
return entries
def main(path):
data = load(path)
raw = data.get("annotations", "")
if not isinstance(raw, str) or not raw.strip():
print("FAIL: annotations is empty. Annotate at least %d lines from your captures."
% MINIMUM)
return 1
if "TODO" in raw:
print("FAIL: annotations still contains TODO placeholders from the starter.")
return 1
entries = parse(raw)
if len(entries) < MINIMUM:
print("FAIL: found %d annotation entries, need at least %d. Each entry starts with a "
"line of the form: - LINE: `<the captured line>`" % (len(entries), MINIMUM))
return 1
seen = set()
for entry in entries:
if entry["line"] in seen:
print("FAIL: the line `%s` is annotated more than once. The %d entries must quote %d "
"DISTINCT lines." % (entry["line"], MINIMUM, MINIMUM))
return 1
seen.add(entry["line"])
haystack = "\n".join(str(data.get(field, "")) for field in CAPTURE_FIELDS)
for entry in entries:
if entry["line"] not in haystack:
print("FAIL: the annotated line `%s` does not appear in curl_v_public, "
"curl_i_redirect, or curl_i_local. Annotate lines you actually captured, and "
"copy them character for character." % entry["line"])
return 1
if not entry["is"]:
print("FAIL: the entry for `%s` has an empty WHAT IT IS. Name the line's job: "
"request line, request header, status line, response header."
% entry["line"])
return 1
words = len(entry["does"].split())
if words < MIN_DOES_WORDS:
print("FAIL: the entry for `%s` has a WHAT IT DOES of %d word(s); at least %d are "
"required. Say what changes because that line is there, not what the line says."
% (entry["line"], words, MIN_DOES_WORDS))
return 1
print("PASS")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))