blob: cb514a26c5f17b6b72d89163fc61a06ae267043f [file] [log] [blame]
#
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Tools for verifying the presence or absence of flags in builds."""
from __future__ import annotations
from pathlib import Path
import shutil
import subprocess
from typing import Optional
from ndk.hosts import Host
import ndk.paths
from ndk.test.spec import BuildConfiguration, CMakeToolchainFile
class FlagVerifierResult:
"""Base class for the result of FlagVerifier checks."""
def __init__(self, error_message: Optional[str]) -> None:
self.error_message = error_message
def failed(self) -> bool:
"""Returns True if verification failed."""
raise NotImplementedError
def make_test_result_tuple(self) -> tuple[bool, Optional[str]]:
"""Creates a test result tuple in the format expect by run_test."""
return not self.failed(), self.error_message
class FlagVerifierSuccess(FlagVerifierResult):
"""A successful flag verification result."""
def __init__(self) -> None:
super().__init__(error_message=None)
def failed(self) -> bool:
return False
class FlagVerifierFailure(FlagVerifierResult):
"""An unsuccessful flag verification result."""
def __init__(self, error_message: str) -> None:
super().__init__(error_message)
def failed(self) -> bool:
return True
class FlagVerifier:
"""Verifies that a build receives the expected flags."""
def __init__(
self, project: Path, ndk_path: Path, config: BuildConfiguration
) -> None:
self.project = project
self.ndk_path = ndk_path
self.abi = config.abi
self.api = config.api
if config.toolchain_file is CMakeToolchainFile.Legacy:
self.toolchain_mode = "ON"
else:
self.toolchain_mode = "OFF"
self.expected_flags: list[str] = []
self.not_expected_flags: list[str] = []
def with_api(self, api: int) -> FlagVerifier:
self.api = api
return self
def expect_flag(self, flag: str) -> None:
"""Verify that the given string is present in the build output.
Args:
flag: The literal string to search for in the output. Will be
matched against whole whitespace-separated words in the
output.
"""
if flag in self.not_expected_flags:
raise ValueError(f"Flag {flag} both expected and not expected")
self.expected_flags.append(flag)
def expect_not_flag(self, flag: str) -> None:
"""Verify that the given string is not present in the build output.
Args:
flag: The literal string to search for in the output. Will be
matched against whole whitespace-separated words in the
output.
"""
if flag in self.expected_flags:
raise ValueError(f"Flag {flag} both expected and not expected")
self.not_expected_flags.append(flag)
def _check_build(self, cmd: list[str]) -> FlagVerifierResult:
result = subprocess.run(
cmd,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="utf-8",
)
if result.returncode != 0:
return FlagVerifierFailure(result.stdout)
words = result.stdout.split(" ")
missing_flags: list[str] = []
wrong_flags: list[str] = []
for expected in self.expected_flags:
if expected not in words:
missing_flags.append(expected)
for not_expected in self.not_expected_flags:
if not_expected in words:
wrong_flags.append(not_expected)
if missing_flags:
return FlagVerifierFailure(
"Expected flags were not present in the build output: "
+ ", ".join(missing_flags)
+ f"\n{result.stdout}"
)
if wrong_flags:
return FlagVerifierFailure(
"Unexpected flags were present in the build output: "
+ ", ".join(wrong_flags)
+ f"\n{result.stdout}"
)
return FlagVerifierSuccess()
def verify(self) -> FlagVerifierResult:
"""Verifies that both ndk-build and CMake behave as specified.
Returns:
A FlagVerifierResult object describing the verification result.
"""
result = self.verify_cmake()
if result.failed():
return result
return self.verify_ndk_build()
def verify_ndk_build(self) -> FlagVerifierResult:
"""Verifies that ndk-build behaves as specified.
Returns:
A FlagVerifierResult object describing the verification result.
"""
ndk_build = self.ndk_path / "ndk-build"
if Host.current() == Host.Windows64:
ndk_build = ndk_build.with_suffix(".cmd")
return self._check_build(
[
str(ndk_build),
"-C",
str(self.project),
"-B",
"V=1",
f"APP_ABI={self.abi}",
f"APP_PLATFORM=android-{self.api}",
]
)
def verify_cmake(
self, cmake_flags: Optional[list[str]] = None
) -> FlagVerifierResult:
"""Verifies that CMake behaves as specified.
Returns:
A FlagVerifierResult object describing the verification result.
"""
if cmake_flags is None:
cmake_flags = []
host = Host.current()
if host == Host.Windows64:
tag = "windows-x86"
else:
tag = f"{host.value}-x86"
cmake = ndk.paths.ANDROID_DIR / f"prebuilts/cmake/{tag}/bin/cmake"
ninja = ndk.paths.ANDROID_DIR / f"prebuilts/ninja/{tag}/ninja"
if host == Host.Windows64:
cmake = cmake.with_suffix(".exe")
ninja = ninja.with_suffix(".exe")
# PythonBuildTest ensures that we're cd'd into the test out directory.
build_dir = Path("build")
if build_dir.exists():
shutil.rmtree(build_dir)
build_dir.mkdir(parents=True)
toolchain_file = self.ndk_path / "build/cmake/android.toolchain.cmake"
cmd = [
str(cmake),
"-S",
str(self.project),
"-B",
str(build_dir),
f"-DCMAKE_TOOLCHAIN_FILE={toolchain_file}",
f"-DANDROID_ABI={self.abi}",
f"-DANDROID_PLATFORM=android-{self.api}",
f"-DANDROID_USE_LEGACY_TOOLCHAIN_FILE={self.toolchain_mode}",
"-GNinja",
f"-DCMAKE_MAKE_PROGRAM={ninja}",
] + cmake_flags
result = subprocess.run(
cmd,
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="utf-8",
)
if result.returncode != 0:
return FlagVerifierFailure(result.stdout)
return self._check_build([str(ninja), "-C", str(build_dir), "-v"])