Skip to content

Commit 4c0eb7f

Browse files
authored
[dotnet] Generate atoms statically (#16608)
1 parent 7a532ba commit 4c0eb7f

File tree

12 files changed

+175
-126
lines changed

12 files changed

+175
-126
lines changed

dotnet/defs.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ load("//dotnet:selenium-dotnet-version.bzl", "SUPPORTED_DEVTOOLS_VERSIONS")
33
load("//dotnet/private:dotnet_nunit_test_suite.bzl", _dotnet_nunit_test_suite = "dotnet_nunit_test_suite")
44
load("//dotnet/private:framework.bzl", _framework = "framework")
55
load("//dotnet/private:generate_devtools.bzl", _generate_devtools = "generate_devtools")
6+
load("//dotnet/private:generate_resources.bzl", _generated_resource_utilities = "generated_resource_utilities")
67
load("//dotnet/private:generated_assembly_info.bzl", _generated_assembly_info = "generated_assembly_info")
78
load("//dotnet/private:nuget_pack.bzl", _nuget_pack = "nuget_pack")
89
load("//dotnet/private:nunit_test.bzl", _nunit_test = "nunit_test")
@@ -19,6 +20,7 @@ csharp_test = _csharp_test
1920
dotnet_nunit_test_suite = _dotnet_nunit_test_suite
2021
framework = _framework
2122
generate_devtools = _generate_devtools
23+
generated_resource_utilities = _generated_resource_utilities
2224
generated_assembly_info = _generated_assembly_info
2325
nuget_pack = _nuget_pack
2426
nunit_test = _nunit_test

dotnet/private/BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
py_binary(
2+
name = "generate_resources_tool",
3+
srcs = ["generate_resources_tool.py"],
4+
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Generate C# partial class with embedded JS resources via a Python tool."""
2+
3+
def _generate_resource_utilities_impl(ctx):
4+
"""Invoke a Python script to generate ResourceUtilities.cs from input files.
5+
6+
The mapping from C# property name to JS file is provided explicitly via the
7+
'resources' attribute as a dict: { "PropertyName": label }.
8+
"""
9+
10+
args = ctx.actions.args()
11+
args.add("--output", ctx.outputs.out)
12+
13+
inputs = []
14+
for target, name in ctx.attr.resources.items():
15+
files = target.files.to_list()
16+
if len(files) != 1:
17+
fail("Each resource label must produce exactly one file, got {} for {}".format(len(files), name))
18+
src = files[0]
19+
inputs.append(src)
20+
args.add("--input")
21+
args.add("%s=%s" % (name, src.path))
22+
23+
ctx.actions.run(
24+
inputs = inputs,
25+
outputs = [ctx.outputs.out],
26+
executable = ctx.executable._tool,
27+
arguments = [args],
28+
mnemonic = "GenerateResourceUtilities",
29+
progress_message = "Generating C# ResourceUtilities partial class",
30+
)
31+
32+
generated_resource_utilities = rule(
33+
implementation = _generate_resource_utilities_impl,
34+
attrs = {
35+
"resources": attr.label_keyed_string_dict(allow_files = True),
36+
"out": attr.output(mandatory = True),
37+
"_tool": attr.label(
38+
default = Label("//dotnet/private:generate_resources_tool"),
39+
executable = True,
40+
cfg = "exec",
41+
),
42+
},
43+
)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python3
2+
"""Generate C# ResourceUtilities partial class with embedded JS resources.
3+
4+
Usage:
5+
generate_resources_tool.py --output path/to/ResourceUtilities.g.cs \
6+
--input Ident1=path/to/file1.js \
7+
--input Ident2=path/to/file2.js ...
8+
9+
Each identifier becomes a const string in ResourceUtilities class.
10+
The content is emitted as a C# raw string literal using 5-quotes.
11+
12+
TODO:
13+
It would be nice to convert this small single-file utility to .NET10/C#,
14+
so it would work like `dotnet run generate_resources.cs -- <args>`.
15+
Meaning .NET developers can easily support it.
16+
"""
17+
18+
import argparse
19+
import os
20+
import sys
21+
from typing import List, Tuple
22+
23+
24+
def parse_args(argv: List[str]) -> argparse.Namespace:
25+
parser = argparse.ArgumentParser()
26+
parser.add_argument("--output", required=True)
27+
parser.add_argument("--input", action="append", default=[], help="IDENT=path")
28+
return parser.parse_args(argv)
29+
30+
31+
def parse_input_spec(spec: str) -> Tuple[str, str]:
32+
if "=" not in spec:
33+
raise ValueError(f"Invalid --input value, expected IDENT=path, got: {spec}")
34+
ident, path = spec.split("=", 1)
35+
ident = ident.strip()
36+
path = path.strip()
37+
if not ident:
38+
raise ValueError(f"Empty identifier in --input value: {spec}")
39+
if not path:
40+
raise ValueError(f"Empty path in --input value: {spec}")
41+
return ident, path
42+
43+
44+
def generate(output: str, inputs: List[Tuple[str, str]]) -> None:
45+
props: List[str] = []
46+
for prop_name, path in inputs:
47+
with open(path, "r", encoding="utf-8") as f:
48+
content = f.read()
49+
# Use a C# raw string literal with five quotes. For a valid raw
50+
# literal, the content must start on a new line and the closing
51+
# quotes must be on their own line as well. We assume the content
52+
# does not contain a sequence of five consecutive double quotes.
53+
#
54+
# Resulting C# will look like:
55+
# """""
56+
# <content>
57+
# """""
58+
literal = '"""""\n' + content + '\n"""""'
59+
props.append(f" internal const string {prop_name} = {literal};")
60+
61+
lines: List[str] = []
62+
lines.append("// <auto-generated />")
63+
lines.append("namespace OpenQA.Selenium.Internal;")
64+
lines.append("")
65+
lines.append("internal static partial class ResourceUtilities")
66+
lines.append("{")
67+
for p in props:
68+
lines.append(p)
69+
lines.append("}")
70+
lines.append("")
71+
72+
os.makedirs(os.path.dirname(output), exist_ok=True)
73+
with open(output, "w", encoding="utf-8", newline="\n") as f:
74+
f.write("\n".join(lines))
75+
76+
77+
def main(argv: List[str]) -> int:
78+
args = parse_args(argv)
79+
inputs: List[Tuple[str, str]] = []
80+
for spec in args.input:
81+
ident, path = parse_input_spec(spec)
82+
inputs.append((ident, path))
83+
generate(args.output, inputs)
84+
return 0
85+
86+
87+
if __name__ == "__main__":
88+
raise SystemExit(main(sys.argv[1:]))

dotnet/src/webdriver/BUILD.bazel

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
load("//common:defs.bzl", "copy_file")
2-
load("//dotnet:defs.bzl", "csharp_library", "devtools_version_targets", "framework", "generated_assembly_info", "nuget_pack")
2+
load("//dotnet:defs.bzl", "csharp_library", "devtools_version_targets", "framework", "generated_assembly_info", "generated_resource_utilities", "nuget_pack")
33
load(
44
"//dotnet:selenium-dotnet-version.bzl",
55
"ASSEMBLY_COMPANY",
@@ -25,10 +25,23 @@ generated_assembly_info(
2525
version = ASSEMBLY_VERSION,
2626
)
2727

28+
generated_resource_utilities(
29+
name = "resource-utilities",
30+
out = "ResourceUtilities.g.cs",
31+
resources = {
32+
"//javascript/atoms/fragments:find-elements.js": "FindElementsAtom",
33+
"//javascript/atoms/fragments:is-displayed.js": "IsDisplayedAtom",
34+
"//javascript/cdp-support:mutation-listener.js": "MutationListenerAtom",
35+
"//javascript/webdriver/atoms:get-attribute.js": "GetAttributeAtom",
36+
"//third_party/js/selenium:webdriver_json": "WebDriverPrefsJson",
37+
},
38+
)
39+
2840
csharp_library(
2941
name = "webdriver-netstandard2.0",
3042
srcs = [
3143
":assembly-info",
44+
":resource-utilities",
3245
] + glob([
3346
"**/*.cs",
3447
]) + devtools_version_targets(),
@@ -38,13 +51,7 @@ csharp_library(
3851
],
3952
langversion = "12.0",
4053
nullable = "enable",
41-
resources = [
42-
"//javascript/atoms/fragments:find-elements.js",
43-
"//javascript/atoms/fragments:is-displayed.js",
44-
"//javascript/cdp-support:mutation-listener.js",
45-
"//javascript/webdriver/atoms:get-attribute.js",
46-
"//third_party/js/selenium:webdriver_json",
47-
],
54+
resources = [],
4855
target_frameworks = [
4956
"netstandard2.0",
5057
],
@@ -66,6 +73,7 @@ csharp_library(
6673
name = "webdriver-net8.0",
6774
srcs = [
6875
":assembly-info",
76+
":resource-utilities",
6977
] + glob([
7078
"**/*.cs",
7179
]) + devtools_version_targets(),
@@ -78,13 +86,7 @@ csharp_library(
7886
],
7987
langversion = "12.0",
8088
nullable = "enable",
81-
resources = [
82-
"//javascript/atoms/fragments:find-elements.js",
83-
"//javascript/atoms/fragments:is-displayed.js",
84-
"//javascript/cdp-support:mutation-listener.js",
85-
"//javascript/webdriver/atoms:get-attribute.js",
86-
"//third_party/js/selenium:webdriver_json",
87-
],
89+
resources = [],
8890
target_frameworks = [
8991
"net8.0",
9092
],
@@ -99,20 +101,15 @@ csharp_library(
99101
name = "webdriver-netstandard2.0-strongnamed",
100102
srcs = [
101103
":assembly-info",
104+
":resource-utilities",
102105
] + glob([
103106
"**/*.cs",
104107
]) + devtools_version_targets(),
105108
out = "WebDriver.StrongNamed",
106109
keyfile = "//dotnet:Selenium.snk",
107110
langversion = "12.0",
108111
nullable = "enable",
109-
resources = [
110-
"//javascript/atoms/fragments:find-elements.js",
111-
"//javascript/atoms/fragments:is-displayed.js",
112-
"//javascript/cdp-support:mutation-listener.js",
113-
"//javascript/webdriver/atoms:get-attribute.js",
114-
"//third_party/js/selenium:webdriver_json",
115-
],
112+
resources = [],
116113
target_frameworks = [
117114
"netstandard2.0",
118115
],
@@ -134,6 +131,7 @@ csharp_library(
134131
name = "webdriver-net8.0-strongnamed",
135132
srcs = [
136133
":assembly-info",
134+
":resource-utilities",
137135
] + glob([
138136
"**/*.cs",
139137
]) + devtools_version_targets(),
@@ -144,13 +142,7 @@ csharp_library(
144142
keyfile = "//dotnet:Selenium.snk",
145143
langversion = "12.0",
146144
nullable = "enable",
147-
resources = [
148-
"//javascript/atoms/fragments:find-elements.js",
149-
"//javascript/atoms/fragments:is-displayed.js",
150-
"//javascript/cdp-support:mutation-listener.js",
151-
"//javascript/webdriver/atoms:get-attribute.js",
152-
"//third_party/js/selenium:webdriver_json",
153-
],
145+
resources = [],
154146
target_frameworks = [
155147
"net8.0",
156148
],

dotnet/src/webdriver/Firefox/FirefoxExtension.cs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
using System.Globalization;
2323
using System.IO;
2424
using System.IO.Compression;
25-
using System.Reflection;
25+
using System.Text;
2626
using System.Text.Json.Nodes;
2727
using System.Xml;
2828

@@ -38,7 +38,6 @@ public class FirefoxExtension
3838
private const string JsonManifestFileName = "manifest.json";
3939

4040
private readonly string extensionFileName;
41-
private readonly string extensionResourceId;
4241

4342
/// <summary>
4443
/// Initializes a new instance of the <see cref="FirefoxExtension"/> class.
@@ -49,27 +48,8 @@ public class FirefoxExtension
4948
/// then using the full path to the file, if a full path is provided.</remarks>
5049
/// <exception cref="ArgumentNullException">If <paramref name="fileName"/> is <see langword="null"/>.</exception>
5150
public FirefoxExtension(string fileName)
52-
: this(fileName, string.Empty)
53-
{
54-
}
55-
56-
/// <summary>
57-
/// Initializes a new instance of the <see cref="FirefoxExtension"/> class.
58-
/// </summary>
59-
/// <param name="fileName">The name of the file containing the Firefox extension.</param>
60-
/// <param name="resourceId">The ID of the resource within the assembly containing the extension
61-
/// if the file is not present in the file system.</param>
62-
/// <remarks>WebDriver attempts to resolve the <paramref name="fileName"/> parameter
63-
/// by looking first for the specified file in the directory of the calling assembly,
64-
/// then using the full path to the file, if a full path is provided. If the file is
65-
/// not found in the file system, WebDriver attempts to locate a resource in the
66-
/// executing assembly with the name specified by the <paramref name="resourceId"/>
67-
/// parameter.</remarks>
68-
/// <exception cref="ArgumentNullException">If <paramref name="fileName"/> or <paramref name="resourceId"/> are <see langword="null"/>.</exception>
69-
internal FirefoxExtension(string fileName, string resourceId)
7051
{
7152
this.extensionFileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
72-
this.extensionResourceId = resourceId ?? throw new ArgumentNullException(nameof(resourceId));
7353
}
7454

7555
/// <summary>
@@ -89,7 +69,7 @@ public void Install(string profileDirectory)
8969

9070
// First, expand the .xpi archive into a temporary location.
9171
Directory.CreateDirectory(tempFileName);
92-
Stream zipFileStream = ResourceUtilities.GetResourceStream(this.extensionFileName, $"{Assembly.GetExecutingAssembly().GetName().Name}.{this.extensionResourceId}");
72+
using Stream zipFileStream = new MemoryStream(Encoding.UTF8.GetBytes(ResourceUtilities.WebDriverPrefsJson));
9373
using (ZipArchive extensionZipArchive = new ZipArchive(zipFileStream, ZipArchiveMode.Read))
9474
{
9575
extensionZipArchive.ExtractToDirectory(tempFileName);

dotnet/src/webdriver/Firefox/FirefoxProfile.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
using System.Diagnostics.CodeAnalysis;
2424
using System.IO;
2525
using System.IO.Compression;
26-
using System.Reflection;
2726
using System.Text.Json;
2827

2928
namespace OpenQA.Selenium.Firefox;
@@ -298,15 +297,12 @@ private void UpdateUserPreferences(string profileDirectory)
298297

299298
private Preferences ReadDefaultPreferences()
300299
{
301-
using (Stream defaultPrefsStream = ResourceUtilities.GetResourceStream("webdriver_prefs.json", $"{Assembly.GetExecutingAssembly().GetName().Name}.webdriver_prefs.json"))
302-
{
303-
using JsonDocument defaultPreferences = JsonDocument.Parse(defaultPrefsStream);
300+
using JsonDocument defaultPreferences = JsonDocument.Parse(ResourceUtilities.WebDriverPrefsJson);
304301

305-
JsonElement immutableDefaultPreferences = defaultPreferences.RootElement.GetProperty("frozen");
306-
JsonElement editableDefaultPreferences = defaultPreferences.RootElement.GetProperty("mutable");
302+
JsonElement immutableDefaultPreferences = defaultPreferences.RootElement.GetProperty("frozen");
303+
JsonElement editableDefaultPreferences = defaultPreferences.RootElement.GetProperty("mutable");
307304

308-
return new Preferences(immutableDefaultPreferences, editableDefaultPreferences);
309-
}
305+
return new Preferences(immutableDefaultPreferences, editableDefaultPreferences);
310306
}
311307

312308
/// <summary>

dotnet/src/webdriver/Internal/ResourceUtilities.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ namespace OpenQA.Selenium.Internal;
2727
/// <summary>
2828
/// Encapsulates methods for finding and extracting WebDriver resources.
2929
/// </summary>
30-
internal static class ResourceUtilities
30+
internal static partial class ResourceUtilities
3131
{
3232
private static string? productVersion;
3333
private static string? platformFamily;

dotnet/src/webdriver/JavaScriptEngine.cs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@
2323
using System.Collections.Generic;
2424
using System.Diagnostics.CodeAnalysis;
2525
using System.Globalization;
26-
using System.IO;
2726
using System.Linq;
28-
using System.Reflection;
2927
using System.Text.Json;
3028
using System.Threading.Tasks;
3129

@@ -142,7 +140,7 @@ public void StopEventMonitoring()
142140
public async Task EnableDomMutationMonitoring()
143141
{
144142
// Execute the script to have it enabled on the currently loaded page.
145-
string script = GetMutationListenerScript();
143+
string script = ResourceUtilities.MutationListenerAtom;
146144
await this.session.Value.Domains.JavaScript.Evaluate(script).ConfigureAwait(false);
147145

148146
await this.AddScriptCallbackBinding(MonitorBindingName).ConfigureAwait(false);
@@ -409,20 +407,6 @@ private async Task EnableDomains()
409407
}
410408
}
411409

412-
private static string GetMutationListenerScript()
413-
{
414-
string listenerScript = string.Empty;
415-
using (Stream resourceStream = ResourceUtilities.GetResourceStream("mutation-listener.js", $"{Assembly.GetExecutingAssembly().GetName().Name}.mutation-listener.js"))
416-
{
417-
using (StreamReader resourceReader = new StreamReader(resourceStream))
418-
{
419-
listenerScript = resourceReader.ReadToEnd();
420-
}
421-
}
422-
423-
return listenerScript;
424-
}
425-
426410
private void OnScriptBindingCalled(object? sender, BindingCalledEventArgs e)
427411
{
428412
if (e.Name == MonitorBindingName)

0 commit comments

Comments
 (0)