Source code

Revision control

Copy as Markdown

Other Tools

import json
import os
import tempfile
from tempfile import TemporaryDirectory
import codegen
import mozpack.path as mozpath
from mozbuild.util import FileAvoidWrite
AUTOGENERATED_HEADER = (
"/* THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */\n\n"
)
def intervention(filename, contents):
if type(contents) is not str:
[bug, origin] = filename.split("-")
contents = {
"label": "example.com",
"bugs": {
f"{bug}": {"matches": [f"*://{origin}/*"], "issue": "blocked-content"}
},
"interventions": [],
} | contents
for i in contents["interventions"]:
if not "platforms" in i:
i["platforms"] = ["all"]
return {
"path": f"data/interventions/{filename}.json",
"contents": contents,
}
def injection(filename, contents):
type = mozpath.splitext(filename)[1].lstrip(".")
return {
"path": f"injections/{type}/{filename}",
"contents": contents,
}
def template(filename, contents):
return {
"path": f"templates/{filename}",
"contents": contents,
}
def test(
description,
harness,
inputs=[],
expected_run_js=None,
expected_generated_files={},
extra_generated_filenames=[],
non_generated_files={},
skip_run_js=False,
):
with TemporaryDirectory(ignore_cleanup_errors=True) as base_addon_dir:
interventions_dir = mozpath.join(base_addon_dir, "data", "interventions")
generated_files_dir = mozpath.join(
base_addon_dir,
"injections",
"generated",
)
templates_dir = mozpath.join(
base_addon_dir,
"templates",
)
os.makedirs(interventions_dir)
os.makedirs(generated_files_dir)
os.makedirs(templates_dir)
injections_dir = mozpath.join(base_addon_dir, "injections")
for ext in ["js", "css"]:
non_generated_type_dir = mozpath.join(injections_dir, ext)
os.makedirs(non_generated_type_dir)
for filename, contents in non_generated_files.items():
ext = mozpath.splitext(filename)[1].lstrip(".")
if ext not in ["css", "js"]:
raise ValueError("Expect only .css or .js files")
full_path = mozpath.join(injections_dir, ext, filename)
with open(full_path, "w") as fd:
fd.write(contents)
for input in inputs:
full_path = mozpath.join(base_addon_dir, input["path"])
contents = input["contents"]
with open(full_path, "w") as fd:
if type(contents) is not str:
json.dump(input["contents"], fd)
else:
fd.write(input["contents"])
run_js_template_path = mozpath.join(base_addon_dir, "run.js")
with open(run_js_template_path, "w") as run_js_template_fd:
run_js_template_fd.write("AVAILABLE_INTERVENTIONS = {}")
preprocessed_filenames = extra_generated_filenames + list(
expected_generated_files.keys()
)
if not skip_run_js:
with tempfile.NamedTemporaryFile(suffix="js") as final_runjs_fd:
with FileAvoidWrite(final_runjs_fd.name) as output_run_js:
codegen.generate_run_js(
output_run_js,
run_js_template_path,
interventions_dir,
*preprocessed_filenames,
)
if expected_run_js is not None:
run_js_contents = final_runjs_fd.read().decode("utf-8")
try:
run_js = run_js_contents.lstrip(
"AVAILABLE_INTERVENTIONS = "
).strip()
run_js = json.loads(run_js)
harness.equals(
run_js,
expected_run_js,
f"test run.js was generated correctly | {description}",
)
except json.decoder.JSONDecodeError:
raise ValueError(f"Could not parse JSON from run.js:\n{run_js}")
for filename, expected_contents in expected_generated_files.items():
generated_path = mozpath.join(generated_files_dir, filename)
with FileAvoidWrite(generated_path) as generated_fd:
codegen.generate_file(generated_fd, interventions_dir)
with open(generated_path) as generated_fd:
actual_contents = generated_fd.read()
final_expected_contents = f"{AUTOGENERATED_HEADER}{expected_contents}"
harness.equals(
actual_contents,
final_expected_contents,
f"{filename} was generated with the correct contents for {description}",
)
def WebCompatBuildTest(harness):
intervention_without_generated_files = intervention(
"100-test.com",
{
"interventions": [
{
"content_scripts": {
"css": ["bug100-test.css"],
"js": ["bug100-test.js"],
},
},
]
},
)
test(
"no extra files generated if none are specified",
harness,
inputs=[
intervention_without_generated_files,
injection("bug100-test.css", "body {}"),
injection("bug100-test.js", "console.log('ok')"),
],
expected_run_js={
"100": intervention_without_generated_files["contents"],
},
)
test(
"expected css and js files are generated, and json is transformed properly",
harness,
non_generated_files={
"generic_needing_logging.js": "{window.__webcompat}",
"generic_not_needing_logging.js": "test;window.__webcompat_spoof_platform",
"bug104-custom-1.js": "bug104customJS1",
"bug104-custom-2.js": "bug104customJS2",
},
inputs=[
template("hide_alerts.js", '{"param:alertsToHide"}'),
template("hide_messages.js", 'if(1){"param:messagesToHide"}'),
template(
"modify_meta_viewport.js", 'try{"param:metaViewportChanges"}catch(_){}'
),
template("log_console_message.js", '{console.info("param:bugInfo"})}'),
intervention(
"100-example.com",
{
"css": {
"test1": "c1 {}",
"test2": "c2 {}",
},
"interventions": [
{"css": ["test1"]},
{
"css": {
"all_frames": True,
"match_origin_as_fallback": True,
"which": ["test1", "test2"],
}
},
{
"content_scripts": {
"js": [
"generic_needing_logging.js",
"generic_not_needing_logging.js",
]
},
},
{
"content_scripts": {
"all_frames": True,
"match_origin_as_fallback": True,
"js": ["generic_not_needing_logging.js"],
},
},
{"hide_alerts": ["alert1"]},
{
"hide_alerts": {
"all_frames": True,
"match_origin_as_fallback": True,
"alerts": ["alert2"],
}
},
{
"hide_messages": {
"container": "container1",
"message": "message1",
}
},
{
"hide_messages": [
{
"container": "container2",
"message": "message2",
}
]
},
{
"hide_messages": {
"all_frames": True,
"match_origin_as_fallback": True,
"messages": [
{
"click_adjacent": "to_click",
"container": "container3",
"message": "message3",
}
],
}
},
{
"modify_meta_viewport": {
"height": None,
"initial-scale": "new-initial-scale",
"interactive-widget": {
"only_if_equals": "==1",
"only_if_not_equals": "!=1",
"value": None,
},
"maximum-scale": {
"only_if_equals": ["==2"],
"only_if_not_equals": ["!=2"],
"value": "new-maximum-scale",
},
"minimum-scale": {
"only_if_equals": "==3",
"value": "new-minimum-scale",
},
"user-scalable": {
"only_if_not_equals": "!=4",
"value": "new-user-scalable",
},
"viewport-fit": {
"only_if_equals": ["==5"],
"value": "new-viewport-fit",
},
"width": {
"only_if_not_equals": ["!=6"],
"value": "new-width",
},
}
},
{
"modify_meta_viewport": {
"all_frames": True,
"match_origin_as_fallback": True,
"modify": {
"height": None,
"width": {
"only_if_not_equals": ["!=7"],
"value": "new-width2",
},
},
}
},
],
},
),
# further test that the console logging script gets advanced bugInfo correctly
intervention(
"101-example.com",
{
"bugs": {
"101": {
"matches": ["*://example.com/*", "*://example2.com/*"],
"issue": "blocked-content",
},
"102": {
"matches": ["*://example3.com/*"],
"issue": "blocked-content",
},
"103": {
"matches": ["*://example2.com/*"],
"issue": "blocked-content",
},
},
"interventions": [
{
"content_scripts": {"js": ["generic_needing_logging.js"]},
},
],
},
),
# test that custom JS (with filenames starting with "bug") are mixed into one generated
# content-script in the order specified by the JSON (if they are mixed with generated scripts).
intervention(
"104-example.com",
{
"bugs": {
"104": {
"matches": ["*://example.com/*"],
"issue": "blocked-content",
},
},
"interventions": [
{
"content_scripts": {
"js": [
"bug104-custom-2.js",
"generic_not_needing_logging.js",
"generic_needing_logging.js",
"bug104-custom-1.js",
]
},
},
{
"content_scripts": {
"js": [
"bug104-custom-2.js",
"bug104-custom-1.js",
]
},
},
],
},
),
],
expected_run_js={
"100": {
"bugs": {
"100": {
"issue": "blocked-content",
"matches": ["*://example.com/*"],
}
},
"interventions": [
{
"content_scripts": {
"css": ["injections/generated/bug100-example.com-test1.css"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"all_frames": True,
"css": [
"injections/generated/bug100-example.com-test1.css",
"injections/generated/bug100-example.com-test2.css",
],
"match_origin_as_fallback": True,
},
"platforms": ["all"],
},
{
"content_scripts": {
"js": ["injections/generated/bug100-example.com.js"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"all_frames": True,
"js": ["injections/generated/bug100-example.com-1.js"],
"match_origin_as_fallback": True,
},
"platforms": ["all"],
},
{
"content_scripts": {
"js": ["injections/generated/bug100-example.com-2.js"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"all_frames": True,
"js": ["injections/generated/bug100-example.com-3.js"],
"match_origin_as_fallback": True,
},
"platforms": ["all"],
},
{
"content_scripts": {
"js": ["injections/generated/bug100-example.com-4.js"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"js": ["injections/generated/bug100-example.com-5.js"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"all_frames": True,
"js": ["injections/generated/bug100-example.com-6.js"],
"match_origin_as_fallback": True,
},
"platforms": ["all"],
},
{
"content_scripts": {
"js": ["injections/generated/bug100-example.com-7.js"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"all_frames": True,
"js": ["injections/generated/bug100-example.com-8.js"],
"match_origin_as_fallback": True,
},
"platforms": ["all"],
},
],
"label": "example.com",
},
"101": {
"bugs": {
"101": {
"issue": "blocked-content",
"matches": ["*://example.com/*", "*://example2.com/*"],
},
"102": {
"issue": "blocked-content",
"matches": ["*://example3.com/*"],
},
"103": {
"issue": "blocked-content",
"matches": ["*://example2.com/*"],
},
},
"interventions": [
{
"content_scripts": {
"js": ["injections/generated/bug101-example.com.js"]
},
"platforms": ["all"],
}
],
"label": "example.com",
},
"104": {
"bugs": {
"104": {
"issue": "blocked-content",
"matches": ["*://example.com/*"],
}
},
"interventions": [
{
"content_scripts": {
"js": ["injections/generated/bug104-example.com.js"]
},
"platforms": ["all"],
},
{
"content_scripts": {
"js": ["bug104-custom-2.js", "bug104-custom-1.js"]
},
"platforms": ["all"],
},
],
"label": "example.com",
},
},
expected_generated_files={
"bug100-example.com-test1.css": "c1 {}",
"bug100-example.com-test2.css": "c2 {}",
"bug100-example.com.js": """{window.__webcompat}\n\n{\n test;window.__webcompat_spoof_platform\n}\n\ndelete window.__webcompat_spoof_platform;\n\n{console.info([["example.com", ["100"]]]})}""",
"bug100-example.com-1.js": "{\n test;window.__webcompat_spoof_platform\n}\n\ndelete window.__webcompat_spoof_platform;",
"bug100-example.com-2.js": '{["alert1"]}',
"bug100-example.com-3.js": '{["alert2"]}',
"bug100-example.com-4.js": 'if(1){[{"container": "container1", "message": "message1"}]}',
"bug100-example.com-5.js": 'if(1){[{"container": "container2", "message": "message2"}]}',
"bug100-example.com-6.js": 'if(1){[{"click_adjacent": "to_click", "container": "container3", "message": "message3"}]}',
"bug100-example.com-7.js": 'try{{"height": null, "initial-scale": "new-initial-scale", "interactive-widget": {"only_if_equals": "==1", "only_if_not_equals": "!=1", "value": null}, "maximum-scale": {"only_if_equals": ["==2"], "only_if_not_equals": ["!=2"], "value": "new-maximum-scale"}, "minimum-scale": {"only_if_equals": "==3", "value": "new-minimum-scale"}, "user-scalable": {"only_if_not_equals": "!=4", "value": "new-user-scalable"}, "viewport-fit": {"only_if_equals": ["==5"], "value": "new-viewport-fit"}, "width": {"only_if_not_equals": ["!=6"], "value": "new-width"}}}catch(_){}',
"bug100-example.com-8.js": 'try{{"height": null, "width": {"only_if_not_equals": ["!=7"], "value": "new-width2"}}}catch(_){}',
"bug101-example.com.js": """{window.__webcompat}\n\n{console.info([["example.com", ["101"]], ["example2.com", ["101", "103"]], ["example3.com", ["102"]]]})}""",
"bug104-example.com.js": """{\n bug104customJS2\n}\n\n{\n test;window.__webcompat_spoof_platform\n}\n\n{window.__webcompat}\n\n{\n bug104customJS1\n}\n\ndelete window.__webcompat_spoof_platform;\n\n{console.info([["example.com", ["104"]]]})}""",
},
)
test(
"filenames are cleaned up to prevent errors saving them on filesystem",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": {
"(\u00e7 /\\testé)": "c1 {}",
},
"interventions": [
{"css": ["(\u00e7 /\\testé)"]},
],
},
),
],
expected_run_js={
"100": {
"label": "example.com",
"bugs": {
"100": {
"matches": ["*://example.com/*"],
"issue": "blocked-content",
}
},
"interventions": [
{
"platforms": ["all"],
"content_scripts": {
"css": [
"injections/generated/bug100-example.com-c_teste.css"
]
},
},
],
}
},
expected_generated_files={
"bug100-example.com-c_teste.css": "c1 {}",
},
)
harness.should_throw(
"build aborts on invalid JSON files",
"ValueError: 100-example.com.json is invalid JSON: Expecting value: line 1 column 1 (char 0)",
lambda: test(
"",
harness,
inputs=[intervention("100-example.com", "invalid JSON")],
),
)
harness.should_throw(
"build aborts on unused non-generated files",
"ValueError: Please remove these files which are not referenced in any intervention JSON file: bug100-test.css",
lambda: test(
"",
harness,
non_generated_files={"bug100-test.css": ""},
),
)
harness.should_throw(
"build aborts if unreferenced non-generated files are in-tree",
"ValueError: Please remove these files which are not referenced in any intervention JSON file: bug100-test.js",
lambda: test(
"",
harness,
non_generated_files={"bug100-test.js": ""},
),
)
test(
"allowed to have generic non-generated js/css files in-tree if they don't start with 'bug'",
lambda: test(
"",
harness,
non_generated_files={"generic-fix.css": ""},
),
)
harness.should_throw(
"build aborts if preprocessed mozbuild is out of sync",
"ValueError: preprocessed_intervention_files.mozbuild is out of date:\nPlease remove: bug100-example.old.com.css\nPlease add: bug100-example.com-test.css",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": {
"test": "c {}",
},
"interventions": [
{"css": ["test"]},
],
},
),
],
extra_generated_filenames=["bug100-example.old.com.css"],
),
)
for [invalid_css_section, exc] in [
[[], "[] is not of type 'object'"],
[{}, "{} does not have enough properties"],
[3, "3 is not of type 'object'"],
["", "'' is not of type 'object'"],
]:
harness.should_throw(
f"build aborts if an invalid value is given for a css section ({invalid_css_section})",
f"ValueError: 100-example.com.json is invalid intervention JSON: {exc}",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": invalid_css_section,
},
),
],
),
)
harness.should_throw(
"build aborts if no CSS fragments are given",
"ValueError: css wanted, but none specified for 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"css": ["test1"],
},
],
},
),
],
),
)
for [invalid_css_section, exc] in [
[{"css": None}, "None is not of type 'object'"],
[{"css": {}}, "{} does not have enough properties"],
]:
harness.should_throw(
f"build aborts if bad CSS fragments are listed in intervention sections ({invalid_css_section})",
f"ValueError: 100-example.com.json is invalid intervention JSON: {exc}",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"css": ["test1"],
},
],
}
| invalid_css_section,
),
],
),
)
for [invalid_css_section, exc] in [
[[], "[] is too short"],
[{}, "'which' is a required property"],
[{"all_frames": True}, "'which' is a required property"],
]:
harness.should_throw(
f"build aborts if intervention specifies invalid value for css section ({invalid_css_section})",
f"ValueError: 100-example.com.json is invalid intervention JSON: {exc}",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": {"test": "c {}"},
"interventions": [
{"css": ["test"]},
{"css": invalid_css_section},
],
},
),
],
),
)
for invalid_css_section in [[3], [True]]:
harness.should_throw(
f"build aborts if intervention specifies invalid value for css section ({invalid_css_section})",
f"ValueError: 100-example.com.json is invalid intervention JSON: {invalid_css_section[0]} is not of type 'string'",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": {"test": "c {}"},
"interventions": [
{"css": ["test"]},
{"css": invalid_css_section},
],
},
),
],
),
)
harness.should_throw(
"build aborts if intervention specifies extra unknown keys in css section",
"ValueError: 100-example.com.json is invalid intervention JSON: Additional properties are not allowed ('junk' was unexpected)",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": {"test": "c {}"},
"interventions": [
{"css": ["test"]},
{"css": {"which": ["test"], "junk": 3}},
],
},
),
],
),
)
harness.should_throw(
"build aborts if detects mixing of values for all_frames/match_origin_as_fallback",
"ValueError: cannot mix value of all_frames in css and content_scripts sections in 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"css": {
"test": "c {}",
},
"interventions": [
{
"content_scripts": {
"all_frames": False,
"match_origin_as_fallback": False,
"js": ["bug100-example.com-whatever.js"],
},
"css": {
"all_frames": True,
"match_origin_as_fallback": True,
"which": ["test"],
},
},
],
},
),
injection("bug100-example.com-whatever.js", "body {}"),
],
expected_generated_files={
"bug100-example.com.js": "",
},
),
)
for [invalid, exc] in [
[
["VALUE"],
"Please use lowercase values for hide_alerts values (not `VALUE`)",
],
[
{"alerts": ["VALUE"]},
"Please use lowercase values for hide_alerts values (not `VALUE`)",
],
]:
harness.should_throw(
"build aborts if hide_alerts is invalid",
f"ValueError: {exc}",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{"hide_alerts": invalid},
],
},
),
],
),
)
for [section, expected, unexpected] in [
["hide_alerts", "alerts", "alertss"],
["hide_messages", "messages", "messagess"],
["modify_meta_viewport", "modify", "mofidy"],
]:
for [invalid, exc] in [
[{"all_frames": "true"}, "'true' is not of type 'boolean'"],
[{"match_origin_as_fallback": []}, "[] is not of type 'boolean'"],
[{expected: True}, "True"],
[{unexpected: ["value"]}, f"{{'{unexpected}': ['value']}} is not valid"],
[{"all_frames": True}, "{'all_frames': True} is not valid"],
[
{"match_origin_as_fallback": True},
"{'match_origin_as_fallback': True} is not valid",
],
[
{"all_frames": True, "match_origin_as_fallback": True},
"{'all_frames': True, 'match_origin_as_fallback': True} is not valid",
],
]:
harness.should_throw(
"build aborts if all_frames or match_origin_as_fallback is invalid",
f"ValueError: 100-example.com.json is invalid intervention JSON: {exc}",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{section: invalid},
],
},
),
],
),
)
harness.should_throw(
"build aborts if no CSS fragments are given",
"ValueError: hide_alerts.js template does not seem to be a proper template",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"hide_alerts": ["test"],
},
],
},
),
template("hide_alerts.js", ""),
],
expected_generated_files={
"bug100-example.com.js": "",
},
),
)
harness.should_throw(
"build aborts if can't find related json for a generated file",
"ValueError: no json intervention file starting with 101-example.com",
lambda: test(
"",
harness,
expected_generated_files={
"bug101-example.com.js": "",
},
skip_run_js=True,
),
)
harness.should_throw(
"build aborts if asked to generated non-css, non-js file",
"ValueError: Do not know how to generate",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"content_scripts": {"css": ["bug100-test.css"]},
}
],
},
),
],
expected_generated_files={
"bug100-example.com.json": "",
},
skip_run_js=True,
),
)
harness.should_throw(
"build aborts if cannot tell which JSON file to use",
"ValueError: multiple json intervention files starting with 100-example.com.. not sure which to use to generate bug100-example.com-test.css from 100-example.com_1.json or 100-example.com_2.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com_1",
{
"css": {
"test": "c {}",
},
"interventions": [
{"css": ["test"]},
],
},
),
intervention(
"100-example.com_2",
{
"css": {
"test": "c {}",
},
"interventions": [
{"css": ["test"]},
],
},
),
],
expected_generated_files={
"bug100-example.com-test.css": "c {}",
},
),
)
harness.should_throw(
"build aborts if has no sources to generate a CSS file",
"ValueError: No needed parts found to generate bug100-example.com.css for 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{"hide_alerts": ["test"]},
],
},
),
],
expected_generated_files={
"bug100-example.com.css": "",
},
skip_run_js=True,
),
)
harness.should_throw(
"build aborts if has no sources to generate a JS file",
"ValueError: No needed parts found to generate bug100-example.com.js for 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"content_scripts": {"css": ["bug100-test.css"]},
}
],
},
),
],
expected_generated_files={
"bug100-example.com.js": "",
},
skip_run_js=True,
),
)
harness.should_throw(
"build aborts if JS template is missing",
"ValueError: Could not access expected template hide_alerts.js",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{"hide_alerts": ["test"]},
],
},
),
],
expected_generated_files={
"bug100-example.com.js": "",
},
skip_run_js=True,
),
)
harness.should_throw(
"build aborts if non-generated content script is missing",
"ValueError: bug100-test.css is not an accessible file in 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{"content_scripts": {"css": ["bug100-test.css"]}},
],
},
),
],
),
)
harness.should_throw(
"build aborts if non-generated content script is missing",
"ValueError: Please remove the unneeded 'injections/css/' from '/injections/css/bug100-test.css' intervention in 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"content_scripts": {
"css": ["/injections/css/bug100-test.css"]
}
},
],
},
),
],
non_generated_files={"bug100-test.css": ""},
),
)
harness.should_throw(
"build aborts if non-generated content script is missing",
"ValueError: bug100-test.js does not end in .css in 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{"content_scripts": {"css": ["bug100-test.js"]}},
],
},
),
],
non_generated_files={"bug100-test.css": ""},
),
)
harness.should_throw(
"build aborts if non-generated content script is missing",
"ValueError: bug100-test.css is listed twice in same intervention in 100-example.com.json",
lambda: test(
"",
harness,
inputs=[
intervention(
"100-example.com",
{
"interventions": [
{
"content_scripts": {
"css": ["bug100-test.css", "bug100-test.css"]
}
},
],
},
),
],
non_generated_files={"bug100-test.css": ""},
),
)