1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car;
18 
19 import android.util.Log;
20 
21 import androidx.test.ext.junit.runners.AndroidJUnit4;
22 import androidx.test.filters.MediumTest;
23 
24 import org.junit.Test;
25 import org.junit.runner.RunWith;
26 
27 import java.lang.reflect.Field;
28 import java.lang.reflect.Modifier;
29 import java.util.Arrays;
30 import java.util.HashMap;
31 import java.util.Map;
32 
33 /**
34  * Validates that diagnostic constants in CarService and Vehicle HAL have the same value
35  * This is an important assumption to validate because we do not perform any mapping between
36  * the two layers, instead relying on the constants on both sides having identical values.
37  */
38 @RunWith(AndroidJUnit4.class)
39 @MediumTest
40 public class CarDiagnosticConstantsTest extends MockedCarTestBase {
41     static final String TAG = CarDiagnosticConstantsTest.class.getSimpleName();
42 
43     static class MismatchException extends Exception {
dumpClass(Class<?> clazz)44         private static String dumpClass(Class<?> clazz) {
45             StringBuilder builder = new StringBuilder(clazz.getName() + "{\n");
46             Arrays.stream(clazz.getFields()).forEach((Field field) -> {
47                 builder.append('\t').append(field.toString()).append('\n');
48             });
49             return builder.append('}').toString();
50         }
51 
logClasses(Class<?> clazz1, Class<?> clazz2)52         private static void logClasses(Class<?> clazz1, Class<?> clazz2) {
53             Log.d(TAG, "MismatchException. class1: " + dumpClass(clazz1));
54             Log.d(TAG, "MismatchException. class2: " + dumpClass(clazz2));
55         }
56 
MismatchException(String message)57         MismatchException(String message) {
58             super(message);
59         }
60 
fieldValueMismatch(Class<?> clazz1, Class<?> clazz2, String name, int value1, int value2)61         static MismatchException fieldValueMismatch(Class<?> clazz1, Class<?> clazz2, String name,
62                 int value1, int value2) {
63             logClasses(clazz1, clazz2);
64             return new MismatchException("In comparison of " + clazz1 + " and " + clazz2 +
65                 " field " + name  + " had different values " + value1 + " vs. " + value2);
66         }
67 
fieldsOnlyInClass1(Class<?> clazz1, Class<?> clazz2, Map<String, Integer> fields)68         static MismatchException fieldsOnlyInClass1(Class<?> clazz1, Class<?> clazz2,
69                 Map<String, Integer> fields) {
70             logClasses(clazz1, clazz2);
71             return new MismatchException("In comparison of " + clazz1 + " and " + clazz2 +
72                 " some fields were only found in the first class:\n" +
73                 fields.keySet().stream().reduce("",
74                     (String s, String t) -> s + "\n" + t));
75         }
76 
fieldOnlyInClass2(Class<?> clazz1, Class<?> clazz2, String field)77         static MismatchException fieldOnlyInClass2(Class<?> clazz1, Class<?> clazz2, String field) {
78             logClasses(clazz1, clazz2);
79             return new MismatchException("In comparison of " + clazz1 + " and " + clazz2 +
80                 " field " + field + " was not found in both classes");
81         }
82     }
83 
isPublicStaticFinalInt(Field field)84     static boolean isPublicStaticFinalInt(Field field) {
85         final int modifiers = field.getModifiers();
86         final boolean isPublic = (modifiers & Modifier.PUBLIC) == Modifier.PUBLIC;
87         final boolean isStatic = (modifiers & Modifier.STATIC) == Modifier.STATIC;
88         final boolean isFinal = (modifiers & Modifier.FINAL) == Modifier.FINAL;
89         if (isPublic && isStatic && isFinal) {
90             return field.getType() == int.class;
91         }
92         return false;
93     }
94 
validateMatch(Class<?> clazz1, Class<?> clazz2)95     static void validateMatch(Class<?> clazz1, Class<?> clazz2) throws Exception {
96         Map<String, Integer> fields = new HashMap<>();
97 
98         // add all the fields in the first class to a map
99         Arrays.stream(clazz1.getFields()).filter(
100             CarDiagnosticConstantsTest::isPublicStaticFinalInt).forEach( (Field field) -> {
101                 final String name = field.getName();
102                 try {
103                     fields.put(name, field.getInt(null));
104                 } catch (IllegalAccessException e) {
105                     // this will practically never happen because we checked that it is a
106                     // public static final field before reading from it
107                     Log.wtf(TAG, String.format("attempt to access field %s threw exception",
108                         field.toString()), e);
109                 }
110             });
111 
112         // check for all fields in the second class, and remove matches from the map
113         for (Field field2 : clazz2.getFields()) {
114             if (isPublicStaticFinalInt(field2)) {
115                 final String name = field2.getName();
116                 if (fields.containsKey(name)) {
117                     try {
118                         final int value2 = field2.getInt(null);
119                         final int value1 = fields.getOrDefault(name, value2+1);
120                         if (value2 != value1) {
121                             throw MismatchException.fieldValueMismatch(clazz1, clazz2,
122                                 field2.getName(), value1, value2);
123                         }
124                         fields.remove(name);
125                     } catch (IllegalAccessException e) {
126                         // this will practically never happen because we checked that it is a
127                         // public static final field before reading from it
128                         Log.wtf(TAG, String.format("attempt to access field %s threw exception",
129                             field2.toString()), e);
130                         throw e;
131                     }
132                 } else {
133                     throw MismatchException.fieldOnlyInClass2(clazz1, clazz2, name);
134                 }
135             }
136         }
137 
138         // if anything is left, we didn't find some fields in the second class
139         if (!fields.isEmpty()) {
140             throw MismatchException.fieldsOnlyInClass1(clazz1, clazz2, fields);
141         }
142     }
143 
144     @Test
testFuelSystemStatus()145     public void testFuelSystemStatus() throws Exception {
146         validateMatch(android.hardware.automotive.vehicle.Obd2FuelSystemStatus.class,
147             android.car.diagnostic.CarDiagnosticEvent.FuelSystemStatus.class);
148     }
149 
testFuelType()150     @Test public void testFuelType() throws Exception {
151         validateMatch(android.hardware.automotive.vehicle.Obd2FuelType.class,
152             android.car.diagnostic.CarDiagnosticEvent.FuelType.class);
153     }
154 
testSecondaryAirStatus()155     @Test public void testSecondaryAirStatus() throws Exception {
156         validateMatch(android.hardware.automotive.vehicle.Obd2SecondaryAirStatus.class,
157             android.car.diagnostic.CarDiagnosticEvent.SecondaryAirStatus.class);
158     }
159 
testIgnitionMonitors()160     @Test public void testIgnitionMonitors() throws Exception {
161         validateMatch(android.hardware.automotive.vehicle.Obd2CommonIgnitionMonitors.class,
162             android.car.diagnostic.CarDiagnosticEvent.CommonIgnitionMonitors.class);
163 
164         validateMatch(android.hardware.automotive.vehicle.Obd2CompressionIgnitionMonitors.class,
165             android.car.diagnostic.CarDiagnosticEvent.CompressionIgnitionMonitors.class);
166 
167         validateMatch(android.hardware.automotive.vehicle.Obd2SparkIgnitionMonitors.class,
168             android.car.diagnostic.CarDiagnosticEvent.SparkIgnitionMonitors.class);
169     }
170 }
171