آزمایش کردن

چندین روش مختلف برای آزمایش کد Starlark در Bazel وجود دارد. این صفحه بهترین روش‌ها و چارچوب‌های فعلی را بر اساس موارد استفاده جمع‌آوری می‌کند.

قوانین تست

Skylib یک چارچوب آزمایشی به نام unittest.bzl برای بررسی رفتار تحلیل زمانی قوانین، مانند اقدامات و ارائه دهندگان آنها دارد. چنین تست هایی "تست های تحلیلی" نامیده می شوند و در حال حاضر بهترین گزینه برای آزمایش عملکرد درونی قوانین هستند.

برخی از هشدارها:

  • ادعاهای تست در داخل بیلد اتفاق می‌افتند، نه یک فرآیند اجراکننده آزمایشی جداگانه. اهدافی که توسط آزمایش ایجاد می شوند باید به گونه ای نامگذاری شوند که با اهداف آزمایشات دیگر یا ساختن برخورد نکنند. خطایی که در حین تست رخ می دهد توسط بازل به عنوان شکستگی ساخت و نه شکست تست تلقی می شود.

  • برای تنظیم قوانین تحت آزمایش و قوانین حاوی اظهارات آزمایشی به مقدار مناسبی از دیگ بخار نیاز است. این دیگ بخار ممکن است در ابتدا دلهره آور به نظر برسد. این کمک می کند که در نظر داشته باشید که ماکروها در مرحله بارگذاری ارزیابی می شوند و اهداف تولید می شوند، در حالی که توابع اجرای قوانین تا بعداً در مرحله تجزیه و تحلیل اجرا نمی شوند.

  • در نظر گرفته شده است که تست های آنالیز نسبتاً کوچک و سبک باشند. برخی از ویژگی‌های چارچوب آزمایش تجزیه و تحلیل محدود به تأیید اهداف با حداکثر تعداد وابستگی گذرا (در حال حاضر 500) است. این به دلیل پیامدهای عملکرد استفاده از این ویژگی ها با آزمایش های بزرگتر است.

اصل اساسی تعریف یک قانون تست است که به قانون تحت آزمون بستگی دارد. این امر به قانون تست دسترسی به ارائه دهندگان قانون تحت آزمون می دهد.

تابع اجرای قانون تست، ادعاها را انجام می دهد. اگر خرابی‌هایی وجود داشته باشد، فوراً با فراخوانی fail() (که خطای ساخت زمان تجزیه و تحلیل را ایجاد می‌کند)، این خطاها با ذخیره کردن خطاها در یک اسکریپت تولید شده که در زمان اجرای آزمایش با شکست مواجه می‌شود، افزایش نمی‌یابد.

برای نمونه اسباب‌بازی مینیمال، به دنبال نمونه‌ای که اقدامات را بررسی می‌کند، به زیر مراجعه کنید.

نمونه حداقلی

//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 )

  • نوع قانون تست آن باید provider_contents_test نامیده شود ( foo_test )

  • برچسب هدف این نوع قانون باید foo_test باشد ( provider_contents_test )

  • تابع پیاده سازی برای قانون تست باید _foo_test_impl نامیده شود ( _provider_contents_test_impl )

  • برچسب‌های اهداف قوانین تحت آزمایش و وابستگی‌های آنها باید با foo_ ( provider_contents_ ) باشد.

توجه داشته باشید که برچسب‌های همه اهداف می‌توانند با برچسب‌های دیگر در همان بسته BUILD تضاد داشته باشند، بنابراین استفاده از یک نام منحصر به فرد برای آزمایش مفید است.

تست شکست

ممکن است تأیید شود که یک قانون با توجه به ورودی های خاص یا در وضعیت خاصی شکست می خورد. این را می توان با استفاده از چارچوب آزمون تجزیه و تحلیل انجام داد:

قانون تست ایجاد شده با 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 را برمی گرداند که نشان دهنده اقدامات ثبت شده توسط هدف تحت آزمایش است.

بررسی رفتار قانون تحت پرچم های مختلف

ممکن است بخواهید بررسی کنید که قانون واقعی شما با توجه به پرچم‌های ساخت خاصی رفتار می‌کند. برای مثال، اگر کاربر مشخص کند، قانون شما ممکن است رفتار متفاوتی داشته باشد:

bazel build //mypkg:real_target -c opt

در مقابل

bazel build //mypkg:real_target -c dbg

در نگاه اول، این کار را می توان با آزمایش هدف تحت آزمایش با استفاده از پرچم های ساخت مورد نظر انجام داد:

bazel test //mypkg:myrules_test -c opt

اما پس از آن غیرممکن می‌شود که مجموعه آزمایشی شما به طور همزمان دارای یک آزمایش باشد که رفتار قانون را تحت -c opt تأیید می‌کند و آزمایش دیگری که رفتار قانون را در -c dbg تأیید می‌کند. هر دو تست نمی توانند در یک بیلد اجرا شوند!

این را می توان با مشخص کردن پرچم های ساخت مورد نظر هنگام تعریف قانون تست حل کرد:

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

به طور معمول، یک هدف تحت آزمایش با توجه به پرچم های ساخت فعلی تجزیه و تحلیل می شود. تعیین config_settings مقادیر گزینه های خط فرمان مشخص شده را لغو می کند. (هر گزینه نامشخصی مقادیر خود را از خط فرمان واقعی حفظ می کند).

در فرهنگ لغت مشخص شده config_settings ، پرچم‌های خط فرمان باید با یک مقدار مکان‌نمای خاص //command_line_option: پیشوند شوند، همانطور که در بالا نشان داده شده است.

اعتبار سنجی مصنوعات

راه های اصلی برای بررسی صحت فایل های تولید شده عبارتند از:

  • می‌توانید یک اسکریپت آزمایشی در پوسته، پایتون یا زبان دیگری بنویسید و یک هدف از نوع قانون *_test مناسب ایجاد کنید.

  • می توانید از یک قانون تخصصی برای نوع آزمایشی که می خواهید انجام دهید استفاده کنید.

استفاده از هدف آزمایشی

ساده ترین راه برای تأیید اعتبار یک مصنوع، نوشتن یک اسکریپت و افزودن یک *_test target به فایل BUILD است. مصنوعات خاصی که می خواهید بررسی کنید باید وابستگی داده های این هدف باشند. اگر منطق اعتبارسنجی شما برای چندین آزمایش قابل استفاده مجدد است، باید اسکریپتی باشد که آرگومان های خط فرمان را می گیرد که توسط ویژگی 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 انجام داد، و اسکریپت کمی بیشتر مستندسازی می شود زیرا از مکان هایی نمادین (برای جایگزینی) به جای موارد عددی (برای آرگومان ها) استفاده می کند.

//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

چارچوب unittest.bzl unittest.bzl را می توان برای آزمایش توابع ابزار (یعنی توابعی که نه ماکرو هستند و نه اجرای قانون) استفاده کرد. به جای استفاده از کتابخانه analysistest تست unittest.bzl ، 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 مراجعه کنید.