Generate build date at invocation time

This commit is contained in:
Jan-Erik Rediger 2022-01-03 15:17:54 +01:00 коммит произвёл Jan-Erik Rediger
Родитель c52e0e71a4
Коммит 9dd0bdde69
9 изменённых файлов: 276 добавлений и 8 удалений

Просмотреть файл

@ -4,6 +4,16 @@
- Support global file-level tags in metrics.yaml ([bug 1745283](https://bugzilla.mozilla.org/show_bug.cgi?id=1745283))
- Glinter: Reject metric files if they use `unit` by mistake. It should be `time_unit` ([#432](https://github.com/mozilla/glean_parser/pull/432)).
- Automatically generate a build date when generating build info ([#431](https://github.com/mozilla/glean_parser/pull/431)).
Enabled for Kotlin and Swift.
This can be changed with the `build_date` command line option.
`build_date=0` will use a static unix epoch time.
`build_date=2022-01-03T17:30:00` will parse the ISO8601 string to use (as a UTC timestamp).
Other values will throw an error.
Example:
glean_parser translate --format kotlin --option build_date=2021-11-01T01:00:00 path/to/metrics.yaml
## 4.3.1

Просмотреть файл

@ -132,6 +132,29 @@ def class_name(obj_type: str) -> str:
return util.Camelize(obj_type) + "MetricType"
def generate_build_date(date: Optional[str]) -> str:
"""
Generate the build timestamp.
"""
ts = util.build_date(date)
data = [
str(ts.year),
# In Java the first month of the year in calendars is JANUARY which is 0.
# In Python it's 1-based
str(ts.month - 1),
str(ts.day),
str(ts.hour),
str(ts.minute),
str(ts.second),
]
components = ", ".join(data)
# DatetimeMetricType takes a `Calendar` instance.
return f'Calendar.getInstance(TimeZone.getTimeZone("GMT+0")).also {{ cal -> cal.set({components}) }}' # noqa
def output_gecko_lookup(
objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
) -> None:
@ -249,6 +272,11 @@ def output_kotlin(
- `with_buildinfo`: If "true" a `GleanBuildInfo.kt` file is generated.
Otherwise generation of that file is skipped.
Defaults to "true".
- `build_date`: If set to `0` a static unix epoch time will be used.
If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
it will use that date.
Other values will throw an error.
If not set it will use the current date & time.
"""
if options is None:
options = {}
@ -257,6 +285,7 @@ def output_kotlin(
glean_namespace = options.get("glean_namespace", "mozilla.components.service.glean")
namespace_package = namespace[: namespace.rfind(".")]
with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
build_date = options.get("build_date", None)
# Write out the special "build info" object
template = util.get_jinja2_template(
@ -264,6 +293,7 @@ def output_kotlin(
)
if with_buildinfo:
build_date = generate_build_date(build_date)
# This filename needs to start with "Glean" so it can never clash with a
# metric category
with (output_dir / "GleanBuildInfo.kt").open("w", encoding="utf-8") as fd:
@ -272,6 +302,7 @@ def output_kotlin(
namespace=namespace,
namespace_package=namespace_package,
glean_namespace=glean_namespace,
build_date=build_date,
)
)
fd.write("\n")

Просмотреть файл

@ -134,6 +134,33 @@ def variable_name(var: str) -> str:
return var
class BuildInfo:
def __init__(self, build_date):
self.build_date = build_date
def generate_build_date(date: Optional[str]) -> str:
"""
Generate the build timestamp.
"""
ts = util.build_date(date)
data = [
("year", ts.year),
("month", ts.month),
("day", ts.day),
("hour", ts.hour),
("minute", ts.minute),
("second", ts.second),
]
# The internal DatetimeMetricType API can take a `DateComponents` object,
# which lets us easily specify the timezone.
components = ", ".join([f"{name}: {val}" for (name, val) in data])
return f'DateComponents(calendar: Calendar.current, timeZone: TimeZone(abbreviation: "UTC"), {components})' # noqa
class Category:
"""
Data struct holding information about a metric to be used in the template.
@ -157,6 +184,14 @@ def output_swift(
- namespace: The namespace to generate metrics in
- glean_namespace: The namespace to import Glean from
- allow_reserved: When True, this is a Glean-internal build
- with_buildinfo: If "true" the `GleanBuildInfo` is generated.
Otherwise generation of that file is skipped.
Defaults to "true".
- build_date: If set to `0` a static unix epoch time will be used.
If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
it will use that date.
Other values will throw an error.
If not set it will use the current date & time.
"""
if options is None:
options = {}
@ -174,6 +209,12 @@ def output_swift(
namespace = options.get("namespace", "GleanMetrics")
glean_namespace = options.get("glean_namespace", "Glean")
with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
build_date = options.get("build_date", None)
build_info = None
if with_buildinfo:
build_date = generate_build_date(build_date)
build_info = BuildInfo(build_date=build_date)
filename = "Metrics.swift"
filepath = output_dir / filename
@ -199,6 +240,7 @@ def output_swift(
namespace=namespace,
glean_namespace=glean_namespace,
allow_reserved=options.get("allow_reserved", False),
build_info=build_info,
)
)
# Jinja2 squashes the final newline, so we explicitly add it

Просмотреть файл

@ -14,14 +14,18 @@ Jinja2 template is not. Please file bugs! #}
package {{ namespace }}
import java.util.Calendar
import java.util.TimeZone
import {{ glean_namespace }}.BuildInfo
import {{ namespace_package }}.BuildConfig
@Suppress("MagicNumber")
internal object GleanBuildInfo {
val buildInfo: BuildInfo by lazy {
BuildInfo(
versionCode = BuildConfig.VERSION_CODE.toString(),
versionName = BuildConfig.VERSION_NAME
versionName = BuildConfig.VERSION_NAME,
buildDate = {{ build_date }}
)
}
}

Просмотреть файл

@ -60,6 +60,16 @@ import {{ glean_namespace }}
// swiftlint:disable force_try
extension {{ namespace }} {
{% if build_info %}
class GleanBuild {
private init() {
// Intentionally left private, no external user can instantiate a new global object.
}
public static let info = BuildInfo(buildDate: {{ build_info.build_date }})
}
{% endif %}
{% for category in categories %}
{% if category.contains_pings %}
class {{ category.name|Camelize }} {

Просмотреть файл

@ -24,6 +24,26 @@ import yaml
if sys.version_info < (3, 7):
import iso8601 # type: ignore
def date_fromisoformat(datestr: str) -> datetime.date:
try:
return iso8601.parse_date(datestr).date()
except iso8601.ParseError:
raise ValueError()
def datetime_fromisoformat(datestr: str) -> datetime.datetime:
try:
return iso8601.parse_date(datestr)
except iso8601.ParseError:
raise ValueError()
else:
def date_fromisoformat(datestr: str) -> datetime.date:
return datetime.date.fromisoformat(datestr)
def datetime_fromisoformat(datestr: str) -> datetime.datetime:
return datetime.datetime.fromisoformat(datestr)
TESTING_MODE = "pytest" in sys.modules
@ -360,13 +380,7 @@ def parse_expires(expires: str) -> datetime.date:
Raises a ValueError in case the string is not properly formatted.
"""
try:
if sys.version_info < (3, 7):
try:
return iso8601.parse_date(expires).date()
except iso8601.ParseError:
raise ValueError()
else:
return datetime.date.fromisoformat(expires)
return date_fromisoformat(expires)
except ValueError:
raise ValueError(
f"Invalid expiration date '{expires}'. "
@ -407,6 +421,31 @@ def validate_expires(expires: str) -> None:
)
def build_date(date: Optional[str]) -> datetime.datetime:
"""
Generate the build timestamp.
If `date` is set to `0` a static unix epoch time will be used.
If `date` it is set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
it will use that date.
Note that any timezone offset will be ignored and UTC will be used.
Otherwise it will throw an error.
If `date` is `None` it will use the current date & time.
"""
if date is not None:
date = str(date)
if date == "0":
ts = datetime.datetime(1970, 1, 1, 0, 0, 0)
else:
ts = datetime_fromisoformat(date).replace(tzinfo=datetime.timezone.utc)
else:
ts = datetime.datetime.utcnow()
return ts
def report_validation_errors(all_objects):
"""
Report any validation errors found to the console.

Просмотреть файл

@ -96,6 +96,84 @@ def test_translate_no_buildinfo(tmpdir):
assert "package Foo" in content
def test_translate_build_date(tmpdir):
"""Test with a custom build date."""
runner = CliRunner()
result = runner.invoke(
__main__.main,
[
"translate",
str(ROOT / "data" / "core.yaml"),
"-o",
str(tmpdir),
"-f",
"kotlin",
"-s",
"namespace=Foo",
"-s",
"build_date=2020-01-01T17:30:00",
"--allow-reserved",
],
)
assert result.exit_code == 0
path = Path(str(tmpdir)) / "GleanBuildInfo.kt"
with path.open(encoding="utf-8") as fd:
content = fd.read()
assert "buildDate = Calendar.getInstance" in content
assert "cal.set(2020, 0, 1, 17, 30" in content
def test_translate_fixed_build_date(tmpdir):
"""Test with a custom build date."""
runner = CliRunner()
result = runner.invoke(
__main__.main,
[
"translate",
str(ROOT / "data" / "core.yaml"),
"-o",
str(tmpdir),
"-f",
"kotlin",
"-s",
"namespace=Foo",
"-s",
"build_date=0",
"--allow-reserved",
],
)
assert result.exit_code == 0
path = Path(str(tmpdir)) / "GleanBuildInfo.kt"
with path.open(encoding="utf-8") as fd:
content = fd.read()
assert "buildDate = Calendar.getInstance" in content
assert "cal.set(1970" in content
def test_translate_borked_build_date(tmpdir):
"""Test with a custom build date."""
runner = CliRunner()
result = runner.invoke(
__main__.main,
[
"translate",
str(ROOT / "data" / "core.yaml"),
"-o",
str(tmpdir),
"-f",
"kotlin",
"-s",
"namespace=Foo",
"-s",
"build_date=1",
"--allow-reserved",
],
)
assert result.exit_code == 1
def test_translate_errors(tmpdir):
"""Test the 'translate' command."""
runner = CliRunner()

Просмотреть файл

@ -88,6 +88,10 @@ def test_parser(tmpdir):
content = fd.read()
assert 'category = ""' in content
with (tmpdir / "GleanBuildInfo.kt").open("r", encoding="utf-8") as fd:
content = fd.read()
assert "buildDate = Calendar.getInstance" in content
run_linters(tmpdir.glob("*.kt"))

Просмотреть файл

@ -49,6 +49,56 @@ def test_parser(tmpdir):
assert "True if the user has set Firefox as the default browser." in content
assert "جمع 搜集" in content
assert 'category: ""' in content
assert "class GleanBuild" in content
assert "BuildInfo(buildDate:" in content
run_linters(tmpdir.glob("*.swift"))
def test_parser_no_build_info(tmpdir):
"""Test translating metrics to Swift files without build info."""
tmpdir = Path(str(tmpdir))
translate.translate(
ROOT / "data" / "core.yaml",
"swift",
tmpdir,
{"with_buildinfo": "false"},
{"allow_reserved": True},
)
assert set(x.name for x in tmpdir.iterdir()) == set(["Metrics.swift"])
# Make sure descriptions made it in
with (tmpdir / "Metrics.swift").open("r", encoding="utf-8") as fd:
content = fd.read()
assert "class GleanBuild" not in content
run_linters(tmpdir.glob("*.swift"))
def test_parser_custom_build_date(tmpdir):
"""Test translating metrics to Swift files without build info."""
tmpdir = Path(str(tmpdir))
translate.translate(
ROOT / "data" / "core.yaml",
"swift",
tmpdir,
{"build_date": "2020-01-01T17:30:00"},
{"allow_reserved": True},
)
assert set(x.name for x in tmpdir.iterdir()) == set(["Metrics.swift"])
# Make sure descriptions made it in
with (tmpdir / "Metrics.swift").open("r", encoding="utf-8") as fd:
content = fd.read()
assert "class GleanBuild" in content
assert "BuildInfo(buildDate:" in content
assert "year: 2020, month: 1, day: 1" in content
run_linters(tmpdir.glob("*.swift"))