Ground Zero
Goal
Build ~/projects/first-site/index.html with the file manager and a text editor. Capture its absolute path, the file:// URL the browser shows when you double-click it, and how many clicks it cost — you will want that number in Unit 2. ---
Type: interactive-form
Unit: 1 — One file, one address
Goal
Build ~/projects/first-site/index.html with the file manager and a text editor. Capture its absolute path, the file:// URL the browser shows when you double-click it, and how many clicks it cost — you will want that number in Unit 2.
How this works
You do this on your own machine. What you hand in is five named fields in starter/submission.txt. Open that file and fill each field as you finish the step that produces it.
- Field names to the left of the colon stay as they are.
- Values may contain colons —
file:///…and a timestamp’s04:25are both fine. - A line starting with
#is ignored.
| Field | What goes in it |
|---|---|
platform | Exactly one of macOS, Linux, WSL |
absolute_path | The full path of your index.html, starting with / |
file_url | The exact text in the browser’s address bar after you double-click the file |
mouse_actions | actions=<n> apps=<n> — distinct clicks/drags/typed names, and how many apps |
ls_la_line | The single ls -la line for your index.html, copied as-is |
Your tasks
- Name home. Mac:
/Users/<you>. Linux and WSL:/home/<you>. Write your platform intoplatform. - Make two folders in the file manager, inside home:
projects, thenfirst-site. Not on the Desktop. Type both names. - Make the file. In a text editor, type one
<h1>line, save it intofirst-siteasindex.html. Lowercasei. Watch that the editor does not add.txt. - Write the full path into
absolute_path. From/toindex.html. No~, noC:\. - Double-click the file. Copy the address bar into
file_url. Copy it; do not rebuild it from memory. - One command:
ls -la ~/projects/first-site. Copy theindex.htmlrow intols_la_line. - Count the cost.
actions=<n> apps=<n>intomouse_actions.
On WSL
Put the project on the Linux side: /home/<you>/projects/first-site/index.html. A path under /mnt/c fails. Count every app you had to use to get a file manager onto that side.
What the scaffolding is for
mouse_actions is the number Unit 2 will beat. Count honestly.
file_url is the first of three addresses this file will have. It works on your machine only.
Expected output
platform: Linux
absolute_path: /home/you/projects/first-site/index.html
file_url: file:///home/you/projects/first-site/index.html
mouse_actions: actions=<your count> apps=<your count>
ls_la_line: -rw-r--r-- 1 you you 12 Jul 29 04:25 index.html
A Mac submission uses /Users/you/…. An extra character after the permissions (a . on Fedora) is fine — leave it in.
Rules
- File manager and editor only for tasks 1–5.
ls -lais the one terminal step. - Do not paste a
$or%prompt into a field. - Do not tidy
ls_la_line. One line, spaces intact. - 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.
submission.txt Download
# Ground Zero — submission
#
# Fill in the five values to the RIGHT of each colon. Do not rename the fields,
# do not reorder them, do not delete any of them.
#
# Lines starting with # are ignored. Only the FIRST colon on a line separates the
# field name from its value, so colons inside a value are fine — file:/// and the
# 04:25 in a timestamp both survive.
#
# Rules that the tests enforce:
# - absolute_path must start with "/" and must NOT use ~, a C:\ drive letter,
# or backslashes. It must end with /projects/first-site/index.html
# - file_url is what the browser's address bar ACTUALLY said. Copy it. It begins
# with file:// — an http:// URL from a tutorial is not evidence.
# - ls_la_line is ONE line, copied character for character. Keep the runs of
# spaces. Do not copy the "total" line or the directory's own row.
# - Do not copy a leading $ or % — that is the machine's prompt, not your output.
# One of: macOS | Linux | WSL
platform:
# The full absolute path of your index.html, starting with /
# macOS: /Users/<you>/projects/first-site/index.html
# Linux, WSL: /home/<you>/projects/first-site/index.html
absolute_path:
# The exact text in the browser address bar after you double-clicked index.html
file_url:
# actions = every distinct click, drag, and typed name it took
# apps = how many separate applications you switched between
mouse_actions: actions= apps=
# The single `ls -la` line for index.html, copied exactly
# (run: ls -la ~/projects/first-site — then copy only the index.html row)
ls_la_line:
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 |
|---|---|---|
| The file exists where it was asked to exist, written as a full path |
Full credit (30): absolute, platform-correct home prefix, exact tail, lowercase | 30 |
| The URL is the one the browser actually showed |
Full credit (25): a | 25 |
The ls -la line is a real listing of the submitted file |
Full credit (25): all of the above, spacing preserved.
Partial (12): a genuine listing line that has been reformatted — spaces collapsed, trailing dot
stripped, fields reordered — but is still recognisably this file's row.
Zero: the directory's own row, the | 25 |
| Honest counts and real captures (craft) | Everything in the submission is copied, not reconstructed, and every field is filled with a value rather than a placeholder.
Full credit (20): all five fields present, no placeholders, no prompt characters, counts present and
plausible.
Partial (10): one field left as a placeholder, or a count given as a single number rather than the
| 20 |
| Total | 100 |
Test cases and grader source
| Test | Checks | Expected | Weight |
|---|---|---|---|
| fields_and_home | All five fields present and non-empty; platform is one of the three; absolute_path is space-free, ends with /projects/first-site/index.html, and its home prefix matches the declared platform (/Users/ for macOS, /home/ for Linux and WSL, never /mnt/); mouse_actions parses as actions=<n> apps=<n> with both ≥ 1. | PASS | 30 |
| url_matches_path | file_url begins file:// and ends with absolute_path; on macOS and Linux it must equal file:// + absolute_path exactly. Catches a URL that points at a different file than the one submitted. | PASS | 25 |
| ls_la_line | ls_la_line is a single long-listing row for a regular file: 10-or-11-character mode string starting -, valid rwx triples, numeric link count, positive numeric size, at least eight fields, and a final field of exactly index.html. | PASS | 20 |
| adversarial_shortcuts | Adversarial. Catches the four plausible shortcuts: (a) a relative path such as projects/first-site/index.html submitted as if it were absolute; (b) a ~-abbreviated path such as ~/projects/first-site/index.html; (c) a Windows drive-letter or backslash path such as C:\Users\you\projects\first-site\index.html; and (d) a file_url that is actually an http:// URL copied from a tutorial — including http://localhost:8000/, http://127.0.0.1:8000/, and any bare path pasted where a URL belongs. | PASS | 25 |
tests/test_1_fields_and_home.py
#!/usr/bin/env python3
"""Test 1 of 4 — weight 30.
Checks that the submission is complete and internally consistent: all five fields
present and non-empty, `platform` is one of the three supported words, the
`absolute_path` ends where the project asked it to end and carries the home prefix
that matches the declared platform, and `mouse_actions` parses as the required
`actions=<n> apps=<n>` pair.
Expected output: PASS
Usage: python3 test_1_fields_and_home.py [path/to/submission.txt]
"""
import os
import re
import sys
TAIL = "/projects/first-site/index.html"
REQUIRED = ["platform", "absolute_path", "file_url", "mouse_actions", "ls_la_line"]
PLATFORMS = {"macos": "macOS", "linux": "Linux", "wsl": "WSL"}
PLACEHOLDERS = ("<you>", "<your count>", "<n>", "<integer>")
def default_submission():
here = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))
def load(path):
fields = {}
try:
handle = open(path, encoding="utf-8")
except OSError as exc:
fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
with handle:
for raw in handle:
line = raw.rstrip("\n").rstrip("\r")
if not line.strip() or line.lstrip().startswith("#"):
continue
if ":" not in line:
continue
key, _, value = line.partition(":")
key = key.strip()
if not re.fullmatch(r"[a-z_]+", key):
continue
fields[key] = value.strip()
return fields
def fail(message):
print("FAIL: " + message)
raise SystemExit(1)
def main():
path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
fields = load(path)
for name in REQUIRED:
if name not in fields:
fail("field '%s' is missing from the submission; all five field names must stay exactly "
"as given in starter/submission.txt" % name)
if not fields[name]:
fail("field '%s' is empty; every field needs a value you read off your own screen" % name)
for token in PLACEHOLDERS:
if token in fields[name]:
fail("field '%s' still contains the template placeholder '%s'" % (name, token))
if fields[name][0] in "$%":
fail("field '%s' starts with '%s' — that is the shell prompt, not your output; the '$' is "
"a command prompt and is not meant to be typed in" % (name, fields[name][0]))
key = fields["platform"].strip().lower()
if key not in PLATFORMS:
fail("platform is '%s'; it must be exactly one of macOS, Linux, WSL" % fields["platform"])
platform = PLATFORMS[key]
path_value = fields["absolute_path"]
if " " in path_value or "\t" in path_value:
fail("absolute_path contains a space; name the folders 'projects' and 'first-site' with no "
"spaces so the path can be typed as one argument later")
if not path_value.endswith(TAIL):
fail("absolute_path is '%s'; it must end with the exact string '%s'" % (path_value, TAIL))
if platform == "macOS":
if not path_value.startswith("/Users/"):
fail("platform is macOS but absolute_path does not start with '/Users/'; macOS home "
"directories live under /Users")
else:
if path_value.startswith("/mnt/"):
fail("platform is %s but absolute_path is under /mnt/, which is the Windows side seen "
"from Linux; Microsoft recommends storing your files in the WSL file system, so this "
"belongs at /home/<you>%s" % (platform, TAIL))
if not path_value.startswith("/home/"):
fail("platform is %s but absolute_path does not start with '/home/'; Linux and WSL home "
"directories live under /home" % platform)
match = re.fullmatch(r"actions\s*=\s*(\d+)\s+apps\s*=\s*(\d+)", fields["mouse_actions"])
if match is None:
fail("mouse_actions is '%s'; it must read exactly 'actions=<n> apps=<n>' with two whole "
"numbers" % fields["mouse_actions"])
actions, apps = int(match.group(1)), int(match.group(2))
if actions < 1:
fail("mouse_actions reports actions=0; creating two folders and a file takes at least one "
"action")
if apps < 1:
fail("mouse_actions reports apps=0; you used at least one application")
print("PASS")
if __name__ == "__main__":
main()
tests/test_2_url_matches_path.py
#!/usr/bin/env python3
"""Test 2 of 4 — weight 25.
Checks that `file_url` and `absolute_path` name the SAME file. The URL must begin
with the file:// scheme and end with the submitted absolute path. On macOS and
Linux the two must line up exactly — file:// followed by the path. On WSL the URL
is allowed to carry extra prefix material before the Linux path, because the
browser and the file sit on opposite sides of the Windows/Linux seam; only the
scheme and the tail are required there.
Percent-encoding in the address bar is decoded before comparison.
Expected output: PASS
Usage: python3 test_2_url_matches_path.py [path/to/submission.txt]
"""
import os
import re
import sys
from urllib.parse import unquote
REQUIRED = ["platform", "absolute_path", "file_url"]
def default_submission():
here = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))
def fail(message):
print("FAIL: " + message)
raise SystemExit(1)
def load(path):
fields = {}
try:
handle = open(path, encoding="utf-8")
except OSError as exc:
fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
with handle:
for raw in handle:
line = raw.rstrip("\n").rstrip("\r")
if not line.strip() or line.lstrip().startswith("#"):
continue
if ":" not in line:
continue
key, _, value = line.partition(":")
key = key.strip()
if not re.fullmatch(r"[a-z_]+", key):
continue
fields[key] = value.strip()
return fields
def main():
path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
fields = load(path)
for name in REQUIRED:
if not fields.get(name):
fail("field '%s' is missing or empty; this test needs platform, absolute_path and "
"file_url" % name)
platform = fields["platform"].strip().lower()
if platform not in ("macos", "linux", "wsl"):
fail("platform is '%s'; it must be exactly one of macOS, Linux, WSL" % fields["platform"])
raw_url = fields["file_url"]
url = unquote(raw_url)
target = fields["absolute_path"]
if not url.startswith("file://"):
fail("file_url is '%s'; the address bar after double-clicking a file on disk begins with "
"'file://'" % raw_url)
if url.rstrip("/") == "file:":
fail("file_url is just the scheme with no path; copy the whole address bar")
if not url.endswith(target):
fail("file_url does not end with absolute_path — the URL and the path name different files. "
"url='%s' path='%s'" % (raw_url, target))
if platform in ("macos", "linux"):
expected = "file://" + target
if url != expected:
fail("on %s the address bar shows the scheme followed directly by the absolute path; "
"expected '%s' but got '%s'" % (fields["platform"], expected, raw_url))
print("PASS")
if __name__ == "__main__":
main()
tests/test_3_ls_la_line.py
#!/usr/bin/env python3
"""Test 3 of 4 — weight 20.
Checks that `ls_la_line` is one row of long-listing output describing a REGULAR
FILE named index.html: a 10- or 11-character mode string beginning with '-', three
valid rwx triples, a whole-number link count, a whole-number size greater than
zero, at least eight whitespace-separated fields, and a final field of exactly
'index.html'.
An eleventh character after the ten permission characters is accepted: on Fedora
and RHEL a trailing '.' is an SELinux security-context marker, and plain Ubuntu
prints ten characters and no dot.
Expected output: PASS
Usage: python3 test_3_ls_la_line.py [path/to/submission.txt]
"""
import os
import re
import sys
TRIPLES = re.compile(r"[-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xtT]")
def default_submission():
here = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))
def fail(message):
print("FAIL: " + message)
raise SystemExit(1)
def load(path):
fields = {}
try:
handle = open(path, encoding="utf-8")
except OSError as exc:
fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
with handle:
for raw in handle:
line = raw.rstrip("\n").rstrip("\r")
if not line.strip() or line.lstrip().startswith("#"):
continue
if ":" not in line:
continue
key, _, value = line.partition(":")
key = key.strip()
if not re.fullmatch(r"[a-z_]+", key):
continue
fields[key] = value.strip()
return fields
def main():
path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
fields = load(path)
line = fields.get("ls_la_line", "")
if not line:
fail("field 'ls_la_line' is missing or empty; run 'ls -la ~/projects/first-site' and copy the "
"index.html row")
if line[0] in "$%":
fail("ls_la_line starts with '%s' — that is the shell prompt, not the listing; the '$' is a "
"command prompt and is not meant to be typed in" % line[0])
if line.startswith("ls ") or line.startswith("ls -"):
fail("ls_la_line contains the command you typed, not the line it printed; copy the output row "
"for index.html")
if line.lower().startswith("total"):
fail("ls_la_line is the 'total' summary line, not a file's row; copy the row that ends in "
"index.html")
parts = line.split()
if len(parts) < 8:
fail("ls_la_line has only %d whitespace-separated fields; a long listing row has at least "
"eight: mode, link count, owner, group, size, and the modification time and name"
% len(parts))
mode = parts[0]
if len(mode) not in (10, 11):
fail("the first field of ls_la_line is '%s' (%d characters); a mode string is 10 characters, "
"or 11 when the system appends a security-context marker" % (mode, len(mode)))
if mode[0] == "d":
fail("the type character of ls_la_line is 'd', so you copied a DIRECTORY's row; index.html is "
"a regular file and its row starts with '-'")
if mode[0] != "-":
fail("the type character of ls_la_line is '%s'; a regular file's row starts with '-'" % mode[0])
if TRIPLES.fullmatch(mode[1:10]) is None:
fail("characters 2-10 of the mode string are '%s', which is not three valid rwx triples; copy "
"the line without editing it" % mode[1:10])
if not parts[1].isdigit():
fail("the second field of ls_la_line is '%s'; that position is the link count and must be a "
"whole number" % parts[1])
name = parts[-1]
if name == "Index.html":
fail("ls_la_line ends in 'Index.html' with a capital I; the project asks for 'index.html'. "
"On Linux and on the WSL Linux filesystem those are two different files")
if name != "index.html":
fail("ls_la_line ends in '%s'; it must be the row for 'index.html'" % name)
if not parts[4].isdigit():
fail("the fifth field of ls_la_line is '%s'; that position is the size in bytes and must be a "
"whole number — the fields run mode, link count, owner, group, size" % parts[4])
if int(parts[4]) <= 0:
fail("ls_la_line reports a size of 0 bytes, so index.html was saved empty; type a line of "
"HTML into it and save again")
print("PASS")
if __name__ == "__main__":
main()
tests/test_4_adversarial_shortcuts.py
#!/usr/bin/env python3
"""Test 4 of 4 — weight 25. ADVERSARIAL.
This test exists to catch four specific shortcuts, each of which produces a
submission that LOOKS finished while proving nothing about the learner's machine:
(a) a RELATIVE path submitted as if it were absolute — for example
'projects/first-site/index.html' or './first-site/index.html'. A path is
absolute only when its first character is '/'.
(b) a '~'-ABBREVIATED path — '~/projects/first-site/index.html'. '~' is an
abbreviation the shell expands to $HOME; this field wants what it expands to.
(c) a WINDOWS-STYLE path — 'C:\\Users\\you\\projects\\first-site\\index.html', a
drive letter, or any backslash used as a separator.
(d) a file_url that is actually an http:// URL copied from a tutorial, including
'http://localhost:8000/' and 'http://127.0.0.1:8000/', or a bare filesystem
path pasted where a URL belongs. Double-clicking a file on disk does not
produce an http:// URL — that comes later in the course, from a server the
learner has not started yet.
Expected output: PASS
Usage: python3 test_4_adversarial_shortcuts.py [path/to/submission.txt]
"""
import os
import re
import sys
DRIVE_LETTER = re.compile(r"^[A-Za-z]:")
def default_submission():
here = os.path.dirname(os.path.abspath(__file__))
return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))
def fail(message):
print("FAIL: " + message)
raise SystemExit(1)
def load(path):
fields = {}
try:
handle = open(path, encoding="utf-8")
except OSError as exc:
fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
with handle:
for raw in handle:
line = raw.rstrip("\n").rstrip("\r")
if not line.strip() or line.lstrip().startswith("#"):
continue
if ":" not in line:
continue
key, _, value = line.partition(":")
key = key.strip()
if not re.fullmatch(r"[a-z_]+", key):
continue
fields[key] = value.strip()
return fields
def check_path(value):
if value.startswith("~"):
fail("absolute_path is '%s', which is a '~'-abbreviated path. '~' is shorthand your shell "
"expands to your home directory; this field wants the expanded form — /Users/<you>/... "
"or /home/<you>/..." % value)
if DRIVE_LETTER.match(value):
fail("absolute_path is '%s', which is a Windows drive-letter path. This course works in the "
"POSIX filesystem: on WSL your project belongs at /home/<you>/projects/first-site/"
"index.html, not on a lettered drive" % value)
if "\\" in value:
fail("absolute_path is '%s' and uses backslashes as separators. POSIX paths separate their "
"steps with '/'" % value)
if not value.startswith("/"):
fail("absolute_path is '%s', which is a RELATIVE path — it has no leading '/', so it is "
"measured from wherever you happen to be standing rather than from the root of the "
"machine. An absolute path begins with '/'" % value)
segments = value.split("/")
if "." in segments or ".." in segments:
fail("absolute_path is '%s' and still contains a '.' or '..' segment. Those are relative "
"movements; resolve them and submit the plain absolute path" % value)
if "//" in value:
fail("absolute_path is '%s' and contains an empty path segment ('//')" % value)
def check_url(value, submitted_path):
lowered = value.lower()
if value == submitted_path:
fail("file_url is identical to absolute_path — a filesystem path with no scheme is not a URL. "
"The address bar shows a scheme first")
if lowered.startswith("http://") or lowered.startswith("https://"):
fail("file_url is '%s', which is an http URL. Double-clicking a file on disk opens it through "
"the file:// scheme; an http:// address means a server answered, and you have not "
"started one. Copy what YOUR address bar said" % value)
for authority in ("://localhost", "://127.0.0.1", "://0.0.0.0"):
if authority in lowered:
fail("file_url points at '%s', which is a running web server, not a file opened from "
"disk. This project has no server in it" % authority.lstrip(":/"))
if not lowered.startswith("file://"):
for token in ("localhost", "127.0.0.1", "0.0.0.0", ":8000"):
if token in lowered:
fail("file_url is '%s', which names a web server rather than a file on disk. Nothing "
"in this project starts a server; copy what YOUR address bar said" % value)
if value.startswith("~") or value.startswith("/") or DRIVE_LETTER.match(value):
fail("file_url is '%s', which is a filesystem path rather than a URL. The address bar "
"shows a scheme first — 'file://' followed by the path" % value)
fail("file_url is '%s' and does not begin with the file:// scheme; that is not the address "
"bar's text" % value)
def main():
path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
fields = load(path)
submitted_path = fields.get("absolute_path", "")
url = fields.get("file_url", "")
if not submitted_path:
fail("field 'absolute_path' is missing or empty")
if not url:
fail("field 'file_url' is missing or empty")
check_path(submitted_path)
check_url(url, submitted_path)
print("PASS")
if __name__ == "__main__":
main()