Skip to content

Commit

Permalink
Generate build date at invocation time
Browse files Browse the repository at this point in the history
  • Loading branch information
badboy committed Jan 12, 2022
1 parent c52e0e7 commit 9dd0bdd
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 8 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions glean_parser/kotlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 = {}
Expand All @@ -257,13 +285,15 @@ 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(
"kotlin.buildinfo.jinja2",
)

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:
Expand All @@ -272,6 +302,7 @@ def output_kotlin(
namespace=namespace,
namespace_package=namespace_package,
glean_namespace=glean_namespace,
build_date=build_date,
)
)
fd.write("\n")
Expand Down
42 changes: 42 additions & 0 deletions glean_parser/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 = {}
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion glean_parser/templates/kotlin.buildinfo.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
)
}
}
10 changes: 10 additions & 0 deletions glean_parser/templates/swift.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }} {
Expand Down
53 changes: 46 additions & 7 deletions glean_parser/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@
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

Expand Down Expand Up @@ -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}'. "
Expand Down Expand Up @@ -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.
Expand Down
78 changes: 78 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions tests/test_kotlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))


Expand Down
Loading

0 comments on commit 9dd0bdd

Please sign in to comment.