blob: 724f26d03cd31bf5a898bbf6c9bf9c1f405a3ece [file] [log] [blame]
#! /usr/bin/perl
##
## Copyright 2019 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.
use File::Basename;
## mockcify version
##
## 0.7.0 Comment out unused mock variables
##
## 0.6.3 Streamline inclusion for headers and source
##
## 0.6.2 Add tBTA_STATUS default value, Cpp type failure log
##
## 0.6.1 Add tBTA_SDP_STATUS default value
##
## 0.6.0 Replace `extern` with `include` for mock_function_count_map
##
## 0.5.0 Add compilation check
##
## 0.4.0 Second re-write
##
## 0.3.2 Remove pragma from source file
##
## 0.3.1 Statically link return value to prevent 'this' pointer in function
##
## 0.3.0 Re-write parser.
##
## 0.2.1 Compilation units only include types and a single related header file
## Alphabetically sort functions by return value
## Add non-primative return data values in structure
##
## 0.2.0 First version
##
my $VERSION = "0.7.0";
use diagnostics;
use strict;
use warnings;
use lib "$ENV{ANDROID_BUILD_TOP}/packages/modules/Bluetooth/system/test/tool";
require 'mockcify_util.pl';
my $YEAR = "2023";
my $TOKEN = "MOCKCIFY_TOKEN";
my $MOCKCIFY_BRACKET_GROUP = "MOCKCIFY_BRACKET_GROUP";
my $CLANG_FORMAT = "/usr/bin/clang-format-13";
my $CC = "g++";
my $LIBCHROME = "../../../../external/libchrome/";
my $COMPILE_SCREEN_ENABLED = 0;
my @structs;
my %function_signature;
my %function_return_types;
my @function_names;
my %function_params;
my %function_param_names;
my %function_param_types;
sub clang_format {
return `$CLANG_FORMAT --style="{ColumnLimit: 10000, PointerAlignment: Left, PointerBindsToType: true, FixNamespaceComments: true }"`;
}
## Create a temp directory for any cruft
my $TMPDIR="/tmp/mockcify";
system("mkdir -p $TMPDIR");
my $OUTDIR = "$TMPDIR/out/";
system("mkdir -p $OUTDIR");
my $INCDIR = "$TMPDIR/include/";
system("mkdir -p $INCDIR");
if (scalar(@ARGV == 0)) {
printf(STDERR "ERROR Must supply at least one argument\n");
exit 1;
}
my $arg = shift @ARGV;
## Check only argument for debug vector
if ($arg =~ /--cla[ng]/) {
exit print clang_format();
} elsif ($arg =~ /--f[ilter]/) {
exit print read_stdin_and_filter_file();
} elsif ($arg =~ /--l[ines]/) {
exit print filter_lines(read_stdin_and_filter_file());
} elsif ($arg =~ /--i[nfo]/) {
my ($incs, $types, $funcs) = parse_info(filter_lines(read_stdin_and_filter_file()));
exit print @{$incs}, @{$types}, @{$funcs};
} elsif ($arg =~ /--co[mpile]/) {
exit compilation_screen("mock_" . shift @ARGV);
} elsif ($arg =~ /--cle[an]/) {
exit system("mv $TMPDIR $TMPDIR.deleted");
} elsif ($arg =~ /--u[nittest]/) {
print(STDERR "unit testing device");
}
sub help {
print <<EOF
Usage:
Specify a namespace on the command line for the shared structure data.
Then pipe the C file on stdin and one source and one header file will
be created based upon the namespace convention
mockcify.pl stack_l2cap_api < stack/l2cap/l2c_api.cc
Output files:
mock_stack_l2cap_api.cc
mock_stack_l2cap_api.h
The tool is not capable of parsing C++ and a workaround is to remove
C++ in the source prior to mock-C-fying the source.
EOF
}
## Only single arg is taken
my $namespace = $arg;
if ($namespace =~ /^--/) {
print(STDERR "ERROR Halting due to ill-formed namespace expression \'$namespace\'\n");
exit -1;
}
###
### Phase 0: Prepare input and output file streams
###
## Default to stdout
my $FH_SRC;
my $FH_HDR;
my $src_filename;
my $hdr_filename;
## If namepace specified then write to that source and header
if ($namespace eq "TESTING") {
$FH_SRC = *STDOUT;
$FH_HDR = *STDOUT;
} else {
$src_filename="mock_" . $namespace . ".cc";
$hdr_filename="mock_" . $namespace . ".h";
open($FH_SRC, ">", $OUTDIR .$src_filename)
or die $!;
open($FH_HDR, ">", $OUTDIR .$hdr_filename)
or die $!;
}
###
### Phase 1: Read input file and apply single line filtering
###
my $text = read_stdin_and_filter_file();
###
### Phase 2: Apply Multiline filters
###
$text = filter_lines($text);
##
## Phase 3: Extract required mock information
##
my ($includes_ref, $typedefs_ref, $functions_ref, $usings_ref, $namespaces_ref) = parse_info($text);
my @includes = @{$includes_ref};
my @typedefs = @{$typedefs_ref};
my @functions = @{$functions_ref};
my @namespaces = @{$namespaces_ref};
my @usings = @{$usings_ref};
@includes = reject_include_list(@includes);
@functions = grep { parse_function_into_components ($_) } @functions;
##
## Phase 4: Output the mocks source and header
##
print_source($FH_SRC);
print_header($FH_HDR);
close ($FH_SRC);
close ($FH_HDR);
## Format the final source code files
if (defined $src_filename) {
system("clang-format", "-i", $OUTDIR . $src_filename);
system("clang-format", "-i", $OUTDIR . $hdr_filename);
}
print(STDERR "Generated files:", $OUTDIR . $src_filename, " ", $OUTDIR . $hdr_filename, "\n");
if ($COMPILE_SCREEN_ENABLED) {
my $rc = compilation_screen("mock_" . $namespace);
exit ($rc == 256) ?1 : 0;
}
sub reject_include_list {
my @incs = ();
foreach (@_) {
next if (/init_flags/);
push(@incs, $_);
}
return @incs;
}
sub compile_screen_failed {
my $src = shift @_;
print STDERR <<EOF
MOCK Compilation is EXPERIMENTAL ONLY
ERROR Failed to compile \'$src\' NOTE: This does not mean
the mock is unusable as the tool only screens the compilation.
There could be one of 3 problems:
1. Undeclared external surface or dependency
2. C++ code or namespaces mixed in with C code
3. An issue with proper mock'ing with mockcify.
EOF
}
sub compilation_screen {
my $base= shift @_;
my $src=$base . ".cc";
my $hdr=$base . ".h";
## Verious external or generated header not needed for mocks
foreach((
"test/mock/mock.h",
"src/init_flags.rs.h",
"src/message_loop_thread.rs.h",
"android/hardware/bluetooth/audio/2.2/IBluetoothAudioProvidersFactory.h",
"android/hardware/bluetooth/audio/2.2/types.h",
)) {
system("mkdir -p $INCDIR". dirname($_));
system("touch $INCDIR/$_");
}
my @incs = (
$INCDIR,
$LIBCHROME,
".",
"audio_hal_interface/",
"include/",
"stack/include/",
"btif/include/",
"internal_include",
"osi/include/",
"test/mock/",
"types/",
);
my @defs = (
"HAS_NO_BDROID_BUILDCFG",
);
my $link="test/mock/$hdr";
unlink "$INCDIR/$link";
symlink "$OUTDIR/$hdr", "$INCDIR/$link";
system("$CC -c -std=c++20 -o /dev/null -D" . join(" -D", @defs) . " -I" . join(" -I", @incs) . " $OUTDIR/$src");
my $rc = $?;
($? == 0)
? printf(STDERR "SUCCESS Compiled unit \'$src\'\n")
: compile_screen_failed($src);
return $rc;
}
###
### Phase 4.1: Print the source compilation unit and the associated structues
###
sub print_source {
my $FH = shift @_;
print_copyright($FH);
print_generated_note($FH);
print_mock_header_include($FH);
print_mock_decl_src($FH);
print_usings($FH);
print_internal_structs($FH);
print_source_namespace_structs($FH);
print_static_return_values($FH);
print_mocked_functions($FH);
print $FH "// END mockcify generation\n";
}
###
### Phase 4.2 Print the header unit to be included with the test
###
sub print_header {
my $FH = shift @_;
print_copyright($FH);
print_pragma($FH);
print_generated_note($FH);
print_mock_decl_hdr($FH);
print_includes($FH);
print_usings($FH);
print_defs($FH);
print_header_test_mock_namespace_structs($FH);
print $FH "// END mockcify generation";
}
sub get_function_param_names {
my $name = shift @_;
my @param_names;
foreach (0..$#{$function_param_names{$name}}) {
my $param_name = $function_param_names{$name}[$_];
my $param_type = $function_param_types{$name}[$_];
if (!defined($param_type)) {
printf(STDERR "Unable to find param type def for $name\n");
next;
}
if ($param_type =~ /unique_ptr/) {
## Wrap name in a move operation
push(@param_names, "std::move($param_name)");
} else {
push(@param_names, $param_name);
}
}
return join(',', @param_names);
}
##
## Parse a function signature into 4 basic components and insert into
## the global hashes and arrays.
## 1. @function return type
## 2. @function name
## 3. %param types
## 4. %param names
##
sub parse_function_into_components {
my $function = shift @_;
## Ensure this is really a function string
assert(substr $function, -1 eq ')');
## Split on first occurrence of open paren to get return
## type and name of function
my ($return_type_and_name, $params) = split '\(', $function, 2;
if (!defined($params)) {
printf(STDERR "WARNING \'params\' is undefined \"$params\" function:\'$function\'\n");
return 0;
}
## Remove input params closing paren
$params=~ s/\).*$//;
## Parse the return type and function name
my ($return_type, $name) = $return_type_and_name =~ /(.*)\s(.*)/;
if (!defined($name)) {
printf(STDERR "WARNING \'name\' is undefined \"$return_type_and_name\" a [con|des]tructor ?\n");
return 0;
}
if ($name =~ /::/) {
printf(STDERR "WARNING \'name\' is unhandled class method \'$name\'\n");
return 0;
}
## Store away complete function signature
$function_signature{$name} = $function;
## Store away the parameter type and names
chomp($params);
$function_params{$name} = $params;
## Parse the parameter types and names
my @param_types;
my @param_names;
## Skip when void keyword used for no parameters
if ($params ne "void") {
## TODO Replace all comma types within angle brackets before split
foreach (split ',', $params) {
s/^\s+//;
if (/\(/) {
## TODO Parameter is a C style function
my @vars;
my @f = split /[\(\)]/;
push(@vars, substr $f[1], 1);
} else {
## Store the type and name
my ($type, $name) = /(.*)\s(.*)/;
push(@param_names, $name);
push(@param_types, $type);
}
}
}
push(@function_names, $name);
$function_return_types{$name} = $return_type;
$function_param_types{$name} = \@param_types;
$function_param_names{$name} = \@param_names;
return 1;
}
##
## Read a file from stdin and does a first pass simple
## filtering that removes single lines.
##
sub read_stdin_and_filter_file {
my @filtered_lines;
my @clang_format=clang_format();
foreach (@clang_format) {
## Update header guards with compiler #pragma for proper
## decision processing of header or source
s/^#ifndef [A-Z_0-9]+_H/#pragma once/;
unless (/^extern/
or /^#define /
or / = \{/
or /^#if /
or /^constexpr/
or /^#ifdef/
or /^#ifndef/
or /^#else/
or /^enum/
or /^static.*;$/
or /^#endif/) {
## Remove any single line C style comments
s:/\*.*\*/::;
push(@filtered_lines, $_);
}
}
return join('', @filtered_lines);
}
sub filter_lines {
$_ = shift @_;
## Remove anonymous namespaces
## $text =~ s/namespace \{.*\n\} \/\/ namespace/\n/sg;
s/namespace \{.*\n\} \/\/ namespace?/\n/sg;
s/namespace \{.?\n\}/\n/g;
## Remove C style comments
s/\s*\/\*(?:(?!\*\/).)*\*\/\n?/\n/sg;
## Remove Cpp style comments
s/\s*\/\/.*//g;
## Remove unnecessary bluetooth osi specific modifier
s/UNUSED_ATTR//g;
## Modify internally defined structure typedefs
s/typedef struct \{.*?\n\} (\w+);/typedef struct $MOCKCIFY_BRACKET_GROUP $1;/sg;
## Modify internally defined structure typedefs
s/typedef struct (\w+) \{.*?\n\} (\w+);/struct $1 $MOCKCIFY_BRACKET_GROUP;/sg;
## Modify internally defined structures
s/struct (\w+) \{.*?\n\};/struct $1 $MOCKCIFY_BRACKET_GROUP;/sg;
## Remove lines only with spaces
s/^\s+$//sg;
return $_;
}
sub parse_info {
if (/\n#pragma once\n/) {
return parse_info_header(shift @_);
} else {
return parse_info_source(shift @_);
}
}
sub parse_info_header {
my (@includes, @typedefs, @functions, @usings, @namespaces);
foreach (split('\n')) {
chomp();
if (/^ /) {
} elsif (/^#include /) {
push(@includes, $_);
} elsif (/^typedef /) {
push @typedefs, $_;
} elsif ($_ =~ /^ *$/) {
# Skip function body indicated by indentation
} elsif ($_ =~ /^}/) {
# Skip function curly bracket closure
} elsif (/^namespace/) {
push @namespaces, $_;
} elsif (/\(/) {
# Add function signature
chomp();
## Remove all function body after signature
s/{.*$//;
## Remove whitespace on both ends
s/^\s+|\s+$//g;
## Ignore locally linked functions
next if (/^static/);
## Reduce all remaining whitespace to a single space
s/\s+/ /g;
## Remove any semi colons
s/;//g;
push(@functions, "$_\n");
} else {
# Not a function. skip
}
}
printf(STDERR "Parsed HEADER lines includes:%d typedefs:%d functions:%d\n",
scalar(@includes), scalar(@typedefs), scalar(@functions));
return (\@includes, \@typedefs, \@functions, \@usings, \@namespaces);
}
sub parse_info_source{
my @s = split('\n', $_);
my (@includes, @typedefs, @functions, @usings, @namespaces);
foreach (@s) {
chomp();
if (/^ /) {
} elsif (/^#include /) {
push @includes, $_;
} elsif (/^typedef /) {
push @typedefs, $_;
} elsif (/^using /) {
push @usings, $_;
} elsif (/^namespace/) {
push @namespaces, $_;
} elsif ($_ =~ /^ *$/) {
# Skip function body indicated by indentation
} elsif ($_ =~ /^}/) {
# Skip function curly bracket closure
} elsif (/\{/) {
# Add function signature
chomp();
## Remove all function body after signature
s/{.*$//;
## Remove whitespace on both ends
s/^\s+|\s+$//g;
## Ignore locally linked functions
next if (/^static/);
## Reduce all remaining whitespace to a single space
s/\s+/ /g;
push(@functions, "$_\n");
} else {
# Not a function. skip
}
}
printf(STDERR "Parsed SOURCE lines includes:%d typedefs:%d functions:%d\n",
scalar(@includes), scalar(@typedefs), scalar(@functions));
return (\@includes, \@typedefs, \@functions, \@usings, \@namespaces);
}
## Returns the default type specified by the function return type.
## These are processed in priority order.
sub get_default_return_value_from_type {
$_ = shift @_;
assert($_ ne '');
if (/^bool/) {
return "false";
} elsif (/\*$/ or /^std::unique_ptr/ or /^std::shared_ptr/) { ## Pointer return val
return "nullptr";
} elsif (/^void/) {
return "";
} elsif (/^std::string/) {
return "std::string()";
} elsif (/^std::list\<entry_t\>::iterator/) {
return "static std::list<entry_t> v";
} elsif (/^std::list\<section_t\>::iterator/) {
return "std::list<section_t>";
} elsif (/reactor_status_t/) {
return "REACTOR_STATUS_DONE";
} elsif (/tL2CAP_LE_RESULT_CODE/) {
return "L2CAP_LE_RESULT_CONN_OK";
} elsif (/std::vector/) {
return "retval";
} elsif (/tBT_TRANSPORT/) {
return "BT_TRANSPORT_BR_EDR";
} elsif (/tSDP_STATUS/) {
return "SDP_SUCCESS";
} elsif (/tGATT_STATUS/) {
return "GATT_SUCCESS";
} elsif (/tHID_STATUS/) {
return "HID_SUCCESS";
} elsif (/future_t\*/) {
return "FUTURE_FAIL";
} elsif(/bt_status_t/) {
return "BT_STATUS_SUCCESS";
} elsif(/.*module_t\*/) {
return "nullptr";
} elsif(/btav_a2dp_codec_index_t/) {
return "BTAV_A2DP_CODEC_INDEX_SOURCE_MIN";
} elsif(/tBTA_SDP_STATUS/) {
return "BTA_SDP_SUCCESS";
} elsif(/tBTA_STATUS/) {
return "BTA_SUCCESS";
} else {
## Decay to int type
return "0";
}
}
##
## Various print output boilerplate
###
sub print_copyright {
my $FH = shift @_;
print $FH <<EOF
/*
* Copyright $YEAR 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.
*/
EOF
}
## Print body of each function
sub print_mocked_functions {
my $FH = shift @_;
print $FH <<EOF;
// Mocked functions, if any
EOF
foreach my $name (sort @function_names) {
my $return_type = $function_return_types{$name};
assert($return_type ne '');
my $return_keyword = $return_type eq "void" ? "" : "return";
my $function_param_names = get_function_param_names($name);
print $FH <<EOF;
$function_signature{$name} {
inc_func_call_count(__func__);
${return_keyword} test::mock::${namespace}::${name}($function_param_names);
}
EOF
}
print $FH <<EOF;
// Mocked functions complete
EOF
}
sub print_static_return_values {
my $FH = shift @_;
print $FH <<EOF;
// Mocked function return values, if any
namespace test {
namespace mock {
namespace $namespace {
EOF
foreach my $name (sort @function_names) {
$name =~ s/\s+$//;
my $return_type = $function_return_types{$name};
assert($return_type ne '');
next if ($return_type eq "void");
my $default_return_value = get_default_return_value_from_type($return_type);
print $FH "${return_type} ${name}::return_value = ${default_return_value};\n";
}
print $FH <<EOF;
} // namespace $namespace
} // namespace mock
} // namespace test
EOF
}
##
## Collection of mocked functions
sub print_source_namespace_structs {
my $FH = shift @_;
print $FH <<EOF;
namespace test {
namespace mock {
namespace $namespace {
// Function state capture and return values, if needed
EOF
foreach my $name (sort @function_names) {
print $FH "struct $name $name;\n";
}
print $FH <<EOF;
} // namespace $namespace
} // namespace mock
} // namespace test
EOF
}
##
## Print the definitions of the various structures for the header files
##
sub print_header_test_mock_namespace_structs {
my $FH = shift @_;
print $FH <<EOF;
namespace test {
namespace mock {
namespace $namespace {
// Shared state between mocked functions and tests
EOF
foreach my $name (sort @function_names) {
my $input_params = $function_params{$name};
my $vars_commented_out_input_params = comment_out_input_vars($input_params);
my $return_type = $function_return_types{$name};
my @param_names = $function_param_names{$name};
assert($return_type ne '');
my $function_param_names = get_function_param_names($name);
my $return_keyword = $return_type eq "void" ? "" : "return";
my $return_statement = $return_type eq "void" ? "" : "return return_value;";
my $return_definition = $return_type eq "void" ? "" : "static $return_type return_value;";
print $FH <<EOF;
// Name: $name
// Params: $input_params
// Return: $return_type
struct $name {
EOF
if ($return_definition ne "") {
print $FH "$return_definition\n";
}
print $FH <<EOF;
std::function<$return_type($input_params)> body{[]($vars_commented_out_input_params){$return_statement}};
$return_type operator()($input_params) { ${return_keyword} body($function_param_names);};
};
extern struct $name $name;
EOF
}
print $FH <<EOF;
} // namespace $namespace
} // namespace mock
} // namespace test
EOF
}
sub print_pragma {
my $FH = shift @_;
print $FH <<EOF
#pragma once
EOF
}
sub print_generated_note {
my $FH = shift @_;
my $gen = scalar(@functions);
print $FH <<EOF;
/*
* Generated mock file from original source file
* Functions generated:$gen
*
* mockcify.pl ver $VERSION
*/
EOF
}
sub print_usings {
my $FH = shift @_;
print $FH <<EOF;
// Original usings
EOF
foreach (sort @usings) {
print $FH $_, "\n";
}
print($FH "\n");;
}
sub print_includes {
my $FH = shift @_;
print $FH <<EOF;
// Original included files, if any
// NOTE: Since this is a mock file with mock definitions some number of
// include files may not be required. The include-what-you-use
// still applies, but crafting proper inclusion is out of scope
// for this effort. This compilation unit may compile as-is, or
// may need attention to prune from (or add to ) the inclusion set.
EOF
foreach (sort @includes) {
print $FH $_, "\n";
}
print($FH "\n");;
}
sub print_mock_header_include {
my $FH = shift @_;
print $FH <<EOF;
// Mock include file to share data between tests and mock
#include "test/mock/mock_${namespace}.h"
EOF
}
sub print_mock_decl_hdr {
my $FH = shift @_;
print $FH <<EOF;
#include <cstdint>
#include <functional>
EOF
}
sub print_mock_decl_src {
my $FH = shift @_;
print $FH <<EOF;
#include <cstdint>
#include "test/common/mock_functions.h"
EOF
}
sub print_defs {
my $FH = shift @_;
print $FH <<EOF;
// Mocked compile conditionals, if any
EOF
}
sub print_internal_structs {
my $FH = shift @_;
print $FH <<EOF;
// Mocked internal structures, if any
EOF
foreach (sort @structs) {
print $FH $_,"\n"};
print $FH "\n";
}
sub assert {
my ($condition, $msg) = @_;
return if $condition;
if (!$msg) {
my ($pkg, $file, $line) = caller(0);
open my $fh, "<", $file;
my @lines = <$fh>;
close $fh;
$msg = "$file:$line: " . $lines[$line - 1];
}
die "Assertion failed: $msg";
}