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