בדיקה

ישנן מספר גישות שונות לבדיקת קוד Starlark ב-Bazel. דף זה אוסף את השיטות המומלצות והמסגרות הנוכחיות לפי שימוש.

כללי בדיקה

ל-Skylib יש מסגרת בדיקה שנקראת unittest.bzl לבדיקת ההתנהגות של ניתוח הכללים בזמן, כמו הפעולות והספקים שלהם. בדיקות כאלה נקראות "ניתוחים", והן כעת האפשרות הטובה ביותר לבדוק תוצאות פנימיות של כללים.

נקודות חשובות:

  • טענות לבדיקה מופיעות בתוך ה-build, ולא תהליך נפרד לבדיקת ריצה. יעדים שנוצרים על ידי הבדיקה חייבים לקבל שמות כך שלא יתנגשו עם יעדים מבדיקות אחרות או מה-build. שגיאה שמתרחשת במהלך הבדיקה נחשבת על ידי Bazel כשבירת בנייה ולא ככשל בבדיקה.

  • כדי להשתמש בכללים שנמצאים בבדיקה ובכללים שמכילים טענות לבדיקה, צריך סכום הוגן של חומר סטנדרטי. הודעת הסטנדרטיות הזו עלולה להיראות מרתיעה תחילה. חשוב לזכור שפקודות מאקרו עוברות הערכה ויעדים שנוצרים במהלך שלב הטעינה. לעומת זאת, פונקציות של הטמעת כללים אינן פועלות עד מאוחר יותר, במהלך שלב הניתוח.

  • בדיקות ניתוח נועדו להיות קטנות וקלות למדי. תכונות מסוימות של מסגרת הבדיקה של הניתוח מוגבלות לאימות היעדים עם מספר מקסימלי של יחסי תלות טרנזיטיביים (כרגע 500). הסיבה לכך היא ההשלכות של שימוש בתכונות אלו עם בדיקות גדולות יותר.

העיקרון הבסיסי הוא הגדרת כלל בדיקה התלוי בכללי הבדיקה. פעולה זו נותנת לכלל הבדיקה גישה לספקים של כלל הבדיקה שמתחתיו.

פונקציית ההטמעה של כלל הבדיקה כוללת טענות. במקרה של כשלים, לא ניתן להעלות אותם מיד על ידי קריאה ל-fail() (פעולה שתגרום לשגיאה של build בזמן הניתוח), אלא על ידי שמירת השגיאות בסקריפט שנוצר נכשל בזמן ביצוע הבדיקה.

לפניכם דוגמה לצעצוע מינימלי ולאחר מכן דוגמה לבדיקת פעולות.

דוגמה מינימלית

//mypkg/myrules.bzl:

MyInfo = provider(fields = {
    "val": "string value",
    "out": "output File",
})

def _myrule_impl(ctx):
    """Rule that just generates a file and returns a provider."""
    out = ctx.actions.declare_file(ctx.label.name + ".out")
    ctx.actions.write(out, "abc")
    return [MyInfo(val="some value", out=out)]

myrule = rule(
    implementation = _myrule_impl,
)

//mypkg/myrules_test.bzl:

load("@bazel_skylib//lib:unittest.bzl", "asserts", "analysistest")
load(":myrules.bzl", "myrule", "MyInfo")

# ==== Check the provider contents ====

def _provider_contents_test_impl(ctx):
    env = analysistest.begin(ctx)

    target_under_test = analysistest.target_under_test(env)
    # If preferred, could pass these values as "expected" and "actual" keyword
    # arguments.
    asserts.equals(env, "some value", target_under_test[MyInfo].val)

    # If you forget to return end(), you will get an error about an analysis
    # test needing to return an instance of AnalysisTestResultInfo.
    return analysistest.end(env)

# Create the testing rule to wrap the test logic. This must be bound to a global
# variable, not called in a macro's body, since macros get evaluated at loading
# time but the rule gets evaluated later, at analysis time. Since this is a test
# rule, its name must end with "_test".
provider_contents_test = analysistest.make(_provider_contents_test_impl)

# Macro to setup the test.
def _test_provider_contents():
    # Rule under test. Be sure to tag 'manual', as this target should not be
    # built using `:all` except as a dependency of the test.
    myrule(name = "provider_contents_subject", tags = ["manual"])
    # Testing rule.
    provider_contents_test(name = "provider_contents_test",
                           target_under_test = ":provider_contents_subject")
    # Note the target_under_test attribute is how the test rule depends on
    # the real rule target.

# Entry point from the BUILD file; macro for running each test case's macro and
# declaring a test suite that wraps them together.
def myrules_test_suite(name):
    # Call all test functions and wrap their targets in a suite.
    _test_provider_contents()
    # ...

    native.test_suite(
        name = name,
        tests = [
            ":provider_contents_test",
            # ...
        ],
    )

//mypkg/BUILD:

load(":myrules.bzl", "myrule")
load(":myrules_test.bzl", "myrules_test_suite")

# Production use of the rule.
myrule(
    name = "mytarget",
)

# Call a macro that defines targets that perform the tests at analysis time,
# and that can be executed with "bazel test" to return the result.
myrules_test_suite(name = "myrules_test")

ניתן להריץ את הבדיקה עם bazel test //mypkg:myrules_test.

מלבד הצהרות load() הראשוניות, יש שני חלקים עיקריים לקובץ:

  • הבדיקות עצמן, כל אחת מהן מורכבת מ-1) פונקציית הטמעה בזמן הניתוח עבור כלל הבדיקה, 2) הצהרה על כלל הבדיקה דרך analysistest.make() ו-3) פונקציית זמן טעינה (מאקרו) להצהרה על כלל הבדיקה (ויחסי התלות שלו) ועל כלל הבדיקה. אם הטענות אינן משתנות בין מקרי מבחן, 1) ו-2), ייתכן שישותפו על ידי מספר מקרי בדיקה.

  • הפונקציה כלי הבדיקה. היא מזהה את פונקציות זמן הטעינה של כל בדיקה, ומצהירה על יעד test_suite שמאחד את כל הבדיקות יחד.

כדי לשמור על עקביות, יש לפעול לפי המוסכמה למתן שמות מומלצת: foo אמורה להופיע בחלק של שם הבדיקה שמתאר את מה שהבדיקה בודקת (provider_contents בדוגמה שלמעלה). לדוגמה, השם של שיטת בדיקה של JUnit הוא testFoo.

אחר כך:

  • המאקרו שיוצר את הבדיקה והיעד בבדיקה צריך להיות בשם _test_foo (_test_provider_contents)

  • סוג כלל הבדיקה שלו צריך להיות בשם foo_test (provider_contents_test)

  • התווית של היעד של סוג הכלל הזה צריכה להיות foo_test (provider_contents_test)

  • פונקציית ההטמעה עבור כלל הבדיקה צריכה להיקרא _foo_test_impl (_provider_contents_test_impl)

  • יש להוסיף את התוויות של יעדי הכללים שנמצאים בבדיקה ואת התלות שלהם בקידומת foo_ (provider_contents_)

שימו לב שהתוויות של כל היעדים עלולות להתנגש עם תוויות אחרות באותה חבילה של ממשק API, כך שכדאי להשתמש בשם ייחודי עבור הבדיקה.

בדיקות כשל

כדאי לוודא שכלל נכשל בהינתן קלט מסוים או במצב מסוים. אפשר לעשות זאת באמצעות מסגרת הבדיקה של הניתוח:

על כלל הבדיקה שנוצר באמצעות analysistest.make לציין את expect_failure:

failure_testing_test = analysistest.make(
    _failure_testing_test_impl,
    expect_failure = True,
)

היישום של כלל הבדיקה צריך להציג טענות לגבי אופי הכישלון שהתרחש (באופן ספציפי, הודעת הכשל):

def _failure_testing_test_impl(ctx):
    env = analysistest.begin(ctx)
    asserts.expect_failure(env, "This rule should never work")
    return analysistest.end(env)

כמו כן, ודא שהיעד שלך בבדיקה מתויג באופן ספציפי כ 'ידני'. ללא זאת, בניית כל היעדים בחבילה שלך באמצעות :all תגרום לבנייה של יעד שנכשל במכוון ולגרום לכשל בבנייה. כאשר היעד הוא 'ידני', המערכת תבנה את היעד במסגרת הבדיקה רק אם צוין במפורש, או כתלות ביעד לא ידני (כגון כלל הבדיקה):

def _test_failure():
    myrule(name = "this_should_fail", tags = ["manual"])

    failure_testing_test(name = "failure_testing_test",
                         target_under_test = ":this_should_fail")

# Then call _test_failure() in the macro which generates the test suite and add
# ":failure_testing_test" to the suite's test targets.

אימות פעולות רשומות

כדאי לכתוב בדיקות עם טענות לגבי הפעולות שהכלל שלך רושם. לדוגמה, שימוש ב-ctx.actions.run(). ניתן לעשות זאת בפונקציית ההטמעה של כלל הבדיקה. דוגמה:

def _inspect_actions_test_impl(ctx):
    env = analysistest.begin(ctx)

    target_under_test = analysistest.target_under_test(env)
    actions = analysistest.target_actions(env)
    asserts.equals(env, 1, len(actions))
    action_output = actions[0].outputs.to_list()[0]
    asserts.equals(
        env, target_under_test.label.name + ".out", action_output.basename)
    return analysistest.end(env)

הערה: analysistest.target_actions(env) מחזירה רשימה של אובייקטים מסוג Action שמייצגים פעולות שנרשמו על ידי היעד שנמצא בבדיקה.

אימות התנהגות הכללים בסימונים שונים

כדאי לוודא שהכלל האמיתי פועל בצורה מסוימת, בהתחשב בסימונים מסוימים של גרסת ה-build. לדוגמה, הכלל עשוי להתנהג באופן שונה אם משתמש מציין:

bazel build //mypkg:real_target -c opt

לעומת

bazel build //mypkg:real_target -c dbg

במבט ראשון ניתן לעשות זאת על ידי בדיקת היעד שנמצא בבדיקה באמצעות סימון ה-build הלא רצוי:

bazel test //mypkg:myrules_test -c opt

עם זאת, חבילת הבדיקה לא יכולה להכיל בו-זמנית בדיקה שמאמתת את ההתנהגות של הכלל ב--c opt ובדיקה נוספת שמאמתת את ההתנהגות של הכלל לפי -c dbg. שתי הבדיקות לא יוכלו לפעול באותו build!

ניתן לפתור זאת על ידי ציון הסימונים המבוקשים של build בעת הגדרת כלל הבדיקה:

myrule_c_opt_test = analysistest.make(
    _myrule_c_opt_test_impl,
    config_settings = {
        "//command_line_option:compilation_mode": "opt",
    },
)

בדרך כלל, יעד שנמצא בבדיקה נבדק על סמך ה-build הנוכחי של ה-build. ציון config_settings יעקוף את הערכים של אפשרויות שורת הפקודה שצוינו. (כל האפשרויות שלא צוינו ישמרו על הערכים שלהן משורת הפקודה עצמה).

במילון config_settings שצוין, יש להוסיף מראש לסימון שורת הפקודה ערך מיוחד של placeholder //command_line_option:, כפי שמוצג למעלה.

מתבצע אימות של פריטי מידע שנוצרו בתהליך פיתוח (Artifact)

הדרך העיקרית לוודא כי הקבצים שנוצרו נכונים:

  • אפשר לכתוב סקריפט בדיקה במעטפת, ב-Python או בשפה אחרת, וליצור יעד מסוג הכלל המתאים: *_test.

  • תוכל להשתמש בכלל מיוחד לסוג הבדיקה שברצונך לבצע.

שימוש ביעד בדיקה

הדרך הפשוטה ביותר לאמת פריט מידע שנוצר בתהליך פיתוח (Artifact) היא לכתוב סקריפט ולהוסיף יעד *_test לקובץ BUILD. פריטי המידע הספציפיים שנוצרים בתהליך פיתוח (Artifact) שרוצים לבדוק צריכים להיות תלויים בנתונים של היעד הזה. אם לוגיקת האימות שלכם מאפשרת שימוש חוזר במספר בדיקות, היא צריכה להיות סקריפט שמשתמש בארגומנטים של שורת פקודה שנשלטים על ידי המאפיין args של יעד הבדיקה. הנה דוגמה שמוודאת שהפלט של myrule שלמעלה הוא "abc".

//mypkg/myrule_validator.sh:

if [ "$(cat $1)" = "abc" ]; then
  echo "Passed"
  exit 0
else
  echo "Failed"
  exit 1
fi

//mypkg/BUILD:

...

myrule(
    name = "mytarget",
)

...

# Needed for each target whose artifacts are to be checked.
sh_test(
    name = "validate_mytarget",
    srcs = [":myrule_validator.sh"],
    args = ["$(location :mytarget.out)"],
    data = [":mytarget.out"],
)

שימוש בכלל מותאם אישית

חלופה מורכבת יותר היא כתיבת הסקריפט של המעטפת כתבנית שיוצרת לפי כלל חדש. התהליך כולל לוגיקה ישירה יותר ועקרון של Starlark, אבל מוביל לקובצי BUILD נקיים יותר. כיתרון צדדי, ניתן לבצע כל ארגומנט של עיבוד מראש ב-Starlark במקום בסקריפט, והסקריפט הוא קצת יותר תיעוד עצמי מאחר שהוא משתמש ב-placeholders סימבוליים (עבור החלפות) במקום במספרים (לארגומנטים).

//mypkg/myrule_validator.sh.template:

if [ "$(cat %TARGET%)" = "abc" ]; then
  echo "Passed"
  exit 0
else
  echo "Failed"
  exit 1
fi

//mypkg/myrule_validation.bzl:

def _myrule_validation_test_impl(ctx):
  """Rule for instantiating myrule_validator.sh.template for a given target."""
  exe = ctx.outputs.executable
  target = ctx.file.target
  ctx.actions.expand_template(output = exe,
                              template = ctx.file._script,
                              is_executable = True,
                              substitutions = {
                                "%TARGET%": target.short_path,
                              })
  # This is needed to make sure the output file of myrule is visible to the
  # resulting instantiated script.
  return [DefaultInfo(runfiles=ctx.runfiles(files=[target]))]

myrule_validation_test = rule(
    implementation = _myrule_validation_test_impl,
    attrs = {"target": attr.label(allow_single_file=True),
             # You need an implicit dependency in order to access the template.
             # A target could potentially override this attribute to modify
             # the test logic.
             "_script": attr.label(allow_single_file=True,
                                   default=Label("//mypkg:myrule_validator"))},
    test = True,
)

//mypkg/BUILD:

...

myrule(
    name = "mytarget",
)

...

# Needed just once, to expose the template. Could have also used export_files(),
# and made the _script attribute set allow_files=True.
filegroup(
    name = "myrule_validator",
    srcs = [":myrule_validator.sh.template"],
)

# Needed for each target whose artifacts are to be checked. Notice that you no
# longer have to specify the output file name in a data attribute, or its
# $(location) expansion in an args attribute, or the label for the script
# (unless you want to override it).
myrule_validation_test(
    name = "validate_mytarget",
    target = ":mytarget",
)

לחלופין, במקום להשתמש בפעולת הרחבת תבנית, ניתן למקם את התבנית בקובץ .bzl בקובץ מחרוזת ולהרחיב אותה במהלך ניתוח הנתונים באמצעות השיטה str.format או %- עיצוב מחדש.

בדיקת כלים של Starlark

אפשר להשתמש במסגרות של Skylibunittest.bzl כדי לבדוק פונקציות של כלי עזר (כלומר, פונקציות שאינן פקודות מאקרו או הטמעות של כללים ). במקום להשתמש ב-unittest.bzl של analysistest ספרייה, unittest. בסוויטות בדיקה שכאלה, אפשר להשתמש בפונקציית הנוחות unittest.suite() כדי להפחית את הסטנדרטיות.

//mypkg/myhelpers.bzl:

def myhelper():
    return "abc"

//mypkg/myhelpers_test.bzl:

load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load(":myhelpers.bzl", "myhelper")

def _myhelper_test_impl(ctx):
  env = unittest.begin(ctx)
  asserts.equals(env, "abc", myhelper())
  return unittest.end(env)

myhelper_test = unittest.make(_myhelper_test_impl)

# No need for a test_myhelper() setup function.

def myhelpers_test_suite(name):
  # unittest.suite() takes care of instantiating the testing rules and creating
  # a test_suite.
  unittest.suite(
    name,
    myhelper_test,
    # ...
  )

//mypkg/BUILD:

load(":myhelpers_test.bzl", "myhelpers_test_suite")

myhelpers_test_suite(name = "myhelpers_tests")

דוגמאות נוספות זמינות בבדיקות של Skylib עצמו.