1# Copyright 2019 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""A test rule that compares two binary files. 16 17The rule uses a Bash command (diff) on Linux/macOS/non-Windows, and a cmd.exe 18command (fc.exe) on Windows (no Bash is required). 19""" 20 21load("//lib:shell.bzl", "shell") 22 23def _runfiles_path(f): 24 if f.root.path: 25 return f.path[len(f.root.path) + 1:] # generated file 26 else: 27 return f.path # source file 28 29def _diff_test_impl(ctx): 30 if ctx.attr.is_windows: 31 test_bin = ctx.actions.declare_file(ctx.label.name + "-test.bat") 32 ctx.actions.write( 33 output = test_bin, 34 content = """@rem Generated by diff_test.bzl, do not edit. 35@echo off 36SETLOCAL ENABLEEXTENSIONS 37SETLOCAL ENABLEDELAYEDEXPANSION 38set MF=%RUNFILES_MANIFEST_FILE:/=\\% 39set PATH=%SYSTEMROOT%\\system32 40set F1={file1} 41set F2={file2} 42if "!F1:~0,9!" equ "external/" (set F1=!F1:~9!) else (set F1=!TEST_WORKSPACE!/!F1!) 43if "!F2:~0,9!" equ "external/" (set F2=!F2:~9!) else (set F2=!TEST_WORKSPACE!/!F2!) 44for /F "tokens=2* usebackq" %%i in (`findstr.exe /l /c:"!F1! " "%MF%"`) do ( 45 set RF1=%%i 46 set RF1=!RF1:/=\\! 47) 48if "!RF1!" equ "" ( 49 if "%RUNFILES_MANIFEST_ONLY%" neq "1" if exist "%RUNFILES_DIR%\\%F1%" ( 50 set RF1="%RUNFILES_DIR%\\%F1%" 51 ) else ( 52 if exist "{file1}" ( 53 set RF1="{file1}" 54 ) 55 ) 56 if "!RF1!" neq "" ( 57 set RF1=!RF1:/=\\! 58 ) else ( 59 echo>&2 ERROR: !F1! not found 60 exit /b 1 61 ) 62) 63for /F "tokens=2* usebackq" %%i in (`findstr.exe /l /c:"!F2! " "%MF%"`) do ( 64 set RF2=%%i 65 set RF2=!RF2:/=\\! 66) 67if "!RF2!" equ "" ( 68 if "%RUNFILES_MANIFEST_ONLY%" neq "1" if exist "%RUNFILES_DIR%\\%F2%" ( 69 set RF2="%RUNFILES_DIR%\\%F2%" 70 ) else ( 71 if exist "{file2}" ( 72 set RF2="{file2}" 73 ) 74 ) 75 if "!RF2!" neq "" ( 76 set RF2=!RF2:/=\\! 77 ) else ( 78 echo>&2 ERROR: !F2! not found 79 exit /b 1 80 ) 81) 82fc.exe 2>NUL 1>NUL /B "!RF1!" "!RF2!" 83if %ERRORLEVEL% neq 0 ( 84 if %ERRORLEVEL% equ 1 ( 85 echo>&2 FAIL: files "{file1}" and "{file2}" differ. {fail_msg} 86 exit /b 1 87 ) else ( 88 fc.exe /B "!RF1!" "!RF2!" 89 exit /b %errorlevel% 90 ) 91) 92""".format( 93 # TODO(arostovtsev): use shell.escape_for_bat when https://github.com/bazelbuild/bazel-skylib/pull/363 is merged 94 fail_msg = ctx.attr.failure_message, 95 file1 = _runfiles_path(ctx.file.file1), 96 file2 = _runfiles_path(ctx.file.file2), 97 ), 98 is_executable = True, 99 ) 100 else: 101 test_bin = ctx.actions.declare_file(ctx.label.name + "-test.sh") 102 ctx.actions.write( 103 output = test_bin, 104 content = r"""#!/usr/bin/env bash 105set -euo pipefail 106F1="{file1}" 107F2="{file2}" 108[[ "$F1" =~ ^external/* ]] && F1="${{F1#external/}}" || F1="$TEST_WORKSPACE/$F1" 109[[ "$F2" =~ ^external/* ]] && F2="${{F2#external/}}" || F2="$TEST_WORKSPACE/$F2" 110if [[ -d "${{RUNFILES_DIR:-/dev/null}}" && "${{RUNFILES_MANIFEST_ONLY:-}}" != 1 ]]; then 111 RF1="$RUNFILES_DIR/$F1" 112 RF2="$RUNFILES_DIR/$F2" 113elif [[ -f "${{RUNFILES_MANIFEST_FILE:-/dev/null}}" ]]; then 114 RF1="$(grep -F -m1 "$F1 " "$RUNFILES_MANIFEST_FILE" | sed 's/^[^ ]* //')" 115 RF2="$(grep -F -m1 "$F2 " "$RUNFILES_MANIFEST_FILE" | sed 's/^[^ ]* //')" 116elif [[ -f "$TEST_SRCDIR/$F1" && -f "$TEST_SRCDIR/$F2" ]]; then 117 RF1="$TEST_SRCDIR/$F1" 118 RF2="$TEST_SRCDIR/$F2" 119else 120 echo >&2 "ERROR: could not find \"{file1}\" and \"{file2}\"" 121 exit 1 122fi 123if ! diff "$RF1" "$RF2"; then 124 echo >&2 "FAIL: files \"{file1}\" and \"{file2}\" differ. "{fail_msg} 125 exit 1 126fi 127""".format( 128 fail_msg = shell.quote(ctx.attr.failure_message), 129 file1 = _runfiles_path(ctx.file.file1), 130 file2 = _runfiles_path(ctx.file.file2), 131 ), 132 is_executable = True, 133 ) 134 return DefaultInfo( 135 executable = test_bin, 136 files = depset(direct = [test_bin]), 137 runfiles = ctx.runfiles(files = [test_bin, ctx.file.file1, ctx.file.file2]), 138 ) 139 140_diff_test = rule( 141 attrs = { 142 "failure_message": attr.string(), 143 "file1": attr.label( 144 allow_single_file = True, 145 mandatory = True, 146 ), 147 "file2": attr.label( 148 allow_single_file = True, 149 mandatory = True, 150 ), 151 "is_windows": attr.bool(mandatory = True), 152 }, 153 test = True, 154 implementation = _diff_test_impl, 155) 156 157def diff_test(name, file1, file2, failure_message = None, **kwargs): 158 """A test that compares two files. 159 160 The test succeeds if the files' contents match. 161 162 Args: 163 name: The name of the test rule. 164 file1: Label of the file to compare to `file2`. 165 file2: Label of the file to compare to `file1`. 166 failure_message: Additional message to log if the files' contents do not match. 167 **kwargs: The [common attributes for tests](https://bazel.build/reference/be/common-definitions#common-attributes-tests). 168 """ 169 _diff_test( 170 name = name, 171 file1 = file1, 172 file2 = file2, 173 failure_message = failure_message, 174 is_windows = select({ 175 "@bazel_tools//src/conditions:host_windows": True, 176 "//conditions:default": False, 177 }), 178 **kwargs 179 ) 180