Add a table data logger for ABT
This CL adds support for a simple, tabular-data logger for dumpsys to be consumed by something that can pretty-print the data. Test: atest DumpsysTableLoggerTest Bug: 220386889 Change-Id: I2db7d6891d4d9a18b72b95432c053efa2d49c2e6
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.dump
|
||||
|
||||
import java.io.PrintWriter
|
||||
|
||||
/**
|
||||
* Utility for logging nice table data to be parsed (and pretty printed) in bugreports. The general
|
||||
* idea here is to feed your nice, table-like data to this class, which embeds the schema and rows
|
||||
* into the dumpsys, wrapped in a known start and stop tags. Later, one can build a simple parser
|
||||
* and pretty-print this data in a table
|
||||
*
|
||||
* Note: Something should be said here about silently eating errors by filtering out malformed
|
||||
* lines. Because this class is expected to be utilized only during a dumpsys, it doesn't feel
|
||||
* most correct to throw an exception here (since an exception can often be the reason that this
|
||||
* class is created). Because of this, [DumpsysTableLogger] will simply filter out invalid lines
|
||||
* based solely on line length. This behavior might need to be revisited in the future.
|
||||
*
|
||||
* USAGE:
|
||||
* Assuming we have some data that would be logged to dumpsys like so:
|
||||
*
|
||||
* ```
|
||||
* 1: field1=val1, field2=val2..., fieldN=valN
|
||||
* //...
|
||||
* M: field1M=val1M, ..., fieldNM
|
||||
* ```
|
||||
*
|
||||
* You can break the `field<n>` values out into a columns spec:
|
||||
* ```
|
||||
* val cols = [field1, field2,...,fieldN]
|
||||
* ```
|
||||
* And then take all of the historical data lines (1 through M), and break them out into their own
|
||||
* lists:
|
||||
* ```
|
||||
* val rows = [
|
||||
* [field10, field20,..., fieldN0],
|
||||
* //...
|
||||
* [field1M, field2M,..., fieldNM]
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Lastly, create a bugreport-unique section name, and use the table logger to write the data to
|
||||
* dumpsys:
|
||||
* ```
|
||||
* val logger = DumpsysTableLogger(uniqueName, cols, rows)
|
||||
* logger.printTableData(pw)
|
||||
* ```
|
||||
*
|
||||
* The expected output in the dumpsys would be:
|
||||
* ```
|
||||
* SystemUI TableSection START: <SectionName>
|
||||
* version 1
|
||||
* col1|col2|...|colN
|
||||
* field10|field20|...|fieldN0
|
||||
* //...
|
||||
* field1M|field2M|...|fieldNM
|
||||
* SystemUI TableSection END: <SectionName>
|
||||
* ```
|
||||
*
|
||||
* @param sectionName A name for the table data section. Should be unique in the bugreport
|
||||
* @param columns Definition for the columns of the table. This should be the same length as all
|
||||
* data rows
|
||||
* @param rows List of rows to be displayed in the table
|
||||
*/
|
||||
class DumpsysTableLogger(
|
||||
private val sectionName: String,
|
||||
private val columns: List<String>,
|
||||
private val rows: List<Row>
|
||||
) {
|
||||
|
||||
fun printTableData(pw: PrintWriter) {
|
||||
printSectionStart(pw)
|
||||
printSchema(pw)
|
||||
printData(pw)
|
||||
printSectionEnd(pw)
|
||||
}
|
||||
|
||||
private fun printSectionStart(pw: PrintWriter) {
|
||||
pw.println(HEADER_PREFIX + sectionName)
|
||||
pw.println("version $VERSION")
|
||||
}
|
||||
|
||||
private fun printSectionEnd(pw: PrintWriter) {
|
||||
pw.println(FOOTER_PREFIX + sectionName)
|
||||
}
|
||||
|
||||
private fun printSchema(pw: PrintWriter) {
|
||||
pw.println(columns.joinToString(separator = SEPARATOR))
|
||||
}
|
||||
|
||||
private fun printData(pw: PrintWriter) {
|
||||
val count = columns.size
|
||||
rows
|
||||
.filter { it.size == count }
|
||||
.forEach { dataLine ->
|
||||
pw.println(dataLine.joinToString(separator = SEPARATOR))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias Row = List<String>
|
||||
|
||||
/**
|
||||
* DO NOT CHANGE! (but if you must...)
|
||||
* 1. Update the version number
|
||||
* 2. Update any consumers to parse the new version
|
||||
*/
|
||||
private const val HEADER_PREFIX = "SystemUI TableSection START: "
|
||||
private const val FOOTER_PREFIX = "SystemUI TableSection END: "
|
||||
private const val SEPARATOR = "|" // TBD
|
||||
private const val VERSION = "1"
|
||||
@@ -46,6 +46,34 @@ open class ConnectivityState {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun tableColumns(): List<String> {
|
||||
return listOf(
|
||||
"connected",
|
||||
"enabled",
|
||||
"activityIn",
|
||||
"activityOut",
|
||||
"level",
|
||||
"iconGroup",
|
||||
"inetCondition",
|
||||
"rssi",
|
||||
"time")
|
||||
}
|
||||
|
||||
protected open fun tableData(): List<String> {
|
||||
return listOf(
|
||||
connected,
|
||||
enabled,
|
||||
activityIn,
|
||||
activityOut,
|
||||
level,
|
||||
iconGroup,
|
||||
inetCondition,
|
||||
rssi,
|
||||
sSDF.format(time)).map {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun copyFrom(other: ConnectivityState) {
|
||||
connected = other.connected
|
||||
enabled = other.enabled
|
||||
|
||||
@@ -828,6 +828,8 @@ public class MobileSignalController extends SignalController<MobileState, Mobile
|
||||
+ (mMobileStatusHistoryIndex + STATUS_HISTORY_SIZE - i) + "): "
|
||||
+ mMobileStatusHistory[i & (STATUS_HISTORY_SIZE - 1)]);
|
||||
}
|
||||
|
||||
dumpTableData(pw);
|
||||
}
|
||||
|
||||
/** Box for QS icon info */
|
||||
|
||||
@@ -162,6 +162,52 @@ internal class MobileState(
|
||||
builder.append("displayInfo=$telephonyDisplayInfo")
|
||||
}
|
||||
|
||||
override fun tableColumns(): List<String> {
|
||||
val columns = listOf("dataSim",
|
||||
"networkName",
|
||||
"networkNameData",
|
||||
"dataConnected",
|
||||
"roaming",
|
||||
"isDefault",
|
||||
"isEmergency",
|
||||
"airplaneMode",
|
||||
"carrierNetworkChangeMode",
|
||||
"userSetup",
|
||||
"dataState",
|
||||
"defaultDataOff",
|
||||
"showQuickSettingsRatIcon",
|
||||
"voiceServiceState",
|
||||
"isInService",
|
||||
"serviceState",
|
||||
"signalStrength",
|
||||
"displayInfo")
|
||||
|
||||
return super.tableColumns() + columns
|
||||
}
|
||||
|
||||
override fun tableData(): List<String> {
|
||||
val columns = listOf(dataSim,
|
||||
networkName,
|
||||
networkNameData,
|
||||
dataConnected,
|
||||
roaming,
|
||||
isDefault,
|
||||
isEmergency,
|
||||
airplaneMode,
|
||||
carrierNetworkChangeMode,
|
||||
userSetup,
|
||||
dataState,
|
||||
defaultDataOff,
|
||||
showQuickSettingsRatIcon(),
|
||||
getVoiceServiceState(),
|
||||
isInService(),
|
||||
serviceState?.minLog() ?: "(null)",
|
||||
signalStrength?.minLog() ?: "(null)",
|
||||
telephonyDisplayInfo).map { it.toString() }
|
||||
|
||||
return super.tableData() + columns
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -22,9 +22,12 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.settingslib.SignalIcon.IconGroup;
|
||||
import com.android.systemui.dump.DumpsysTableLogger;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
@@ -193,20 +196,68 @@ public abstract class SignalController<T extends ConnectivityState, I extends Ic
|
||||
pw.println(" - " + mTag + " -----");
|
||||
pw.println(" Current State: " + mCurrentState);
|
||||
if (RECORD_HISTORY) {
|
||||
// Count up the states that actually contain time stamps, and only display those.
|
||||
int size = 0;
|
||||
for (int i = 0; i < HISTORY_SIZE; i++) {
|
||||
if (mHistory[i].time != 0) size++;
|
||||
}
|
||||
// Print out the previous states in ordered number.
|
||||
for (int i = mHistoryIndex + HISTORY_SIZE - 1;
|
||||
i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
|
||||
pw.println(" Previous State(" + (mHistoryIndex + HISTORY_SIZE - i) + "): "
|
||||
+ mHistory[i & (HISTORY_SIZE - 1)]);
|
||||
List<ConnectivityState> history = getOrderedHistoryExcludingCurrentState();
|
||||
for (int i = 0; i < history.size(); i++) {
|
||||
pw.println(" Previous State(" + (i + 1) + "): " + mHistory[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mHistory is a ring, so use this method to get the time-ordered (from youngest to oldest)
|
||||
* list of historical states. Filters out any state whose `time` is `0`.
|
||||
*
|
||||
* For ease of compatibility, this list returns JUST the historical states, not the current
|
||||
* state which has yet to be copied into the history
|
||||
*
|
||||
* @see #getOrderedHistory()
|
||||
* @return historical states, ordered from newest to oldest
|
||||
*/
|
||||
List<ConnectivityState> getOrderedHistoryExcludingCurrentState() {
|
||||
ArrayList<ConnectivityState> history = new ArrayList<>();
|
||||
|
||||
// Count up the states that actually contain time stamps, and only display those.
|
||||
int size = 0;
|
||||
for (int i = 0; i < HISTORY_SIZE; i++) {
|
||||
if (mHistory[i].time != 0) size++;
|
||||
}
|
||||
// Print out the previous states in ordered number.
|
||||
for (int i = mHistoryIndex + HISTORY_SIZE - 1;
|
||||
i >= mHistoryIndex + HISTORY_SIZE - size; i--) {
|
||||
history.add(mHistory[i & (HISTORY_SIZE - 1)]);
|
||||
}
|
||||
|
||||
return history;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ordered history states, including the current yet-to-be-copied state. Useful for
|
||||
* logging
|
||||
*
|
||||
* @see #getOrderedHistoryExcludingCurrentState()
|
||||
* @return [currentState, historicalState...] array
|
||||
*/
|
||||
List<ConnectivityState> getOrderedHistory() {
|
||||
ArrayList<ConnectivityState> history = new ArrayList<>();
|
||||
// Start with the current state
|
||||
history.add(mCurrentState);
|
||||
history.addAll(getOrderedHistoryExcludingCurrentState());
|
||||
return history;
|
||||
}
|
||||
|
||||
void dumpTableData(PrintWriter pw) {
|
||||
List<List<String>> tableData = new ArrayList<List<String>>();
|
||||
List<ConnectivityState> history = getOrderedHistory();
|
||||
for (int i = 0; i < history.size(); i++) {
|
||||
tableData.add(history.get(i).tableData());
|
||||
}
|
||||
|
||||
DumpsysTableLogger logger =
|
||||
new DumpsysTableLogger(mTag, mCurrentState.tableColumns(), tableData);
|
||||
|
||||
logger.printTableData(pw);
|
||||
}
|
||||
|
||||
final void notifyListeners() {
|
||||
notifyListeners(mCallbackHandler);
|
||||
}
|
||||
|
||||
@@ -241,6 +241,7 @@ public class WifiSignalController extends SignalController<WifiState, IconGroup>
|
||||
public void dump(PrintWriter pw) {
|
||||
super.dump(pw);
|
||||
mWifiTracker.dump(pw);
|
||||
dumpTableData(pw);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,6 +48,30 @@ internal class WifiState(
|
||||
.append(",subId=").append(subId)
|
||||
}
|
||||
|
||||
override fun tableColumns(): List<String> {
|
||||
val columns = listOf("ssid",
|
||||
"isTransient",
|
||||
"isDefault",
|
||||
"statusLabel",
|
||||
"isCarrierMerged",
|
||||
"subId")
|
||||
|
||||
return super.tableColumns() + columns
|
||||
}
|
||||
|
||||
override fun tableData(): List<String> {
|
||||
val data = listOf(ssid,
|
||||
isTransient,
|
||||
isDefault,
|
||||
statusLabel,
|
||||
isCarrierMerged,
|
||||
subId).map {
|
||||
it.toString()
|
||||
}
|
||||
|
||||
return super.tableData() + data
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
*/
|
||||
|
||||
package com.android.systemui.dump
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
@SmallTest
|
||||
class DumpsysTableLoggerTest : SysuiTestCase() {
|
||||
private val logger = DumpsysTableLogger(
|
||||
TEST_SECTION_NAME,
|
||||
TEST_COLUMNS,
|
||||
TEST_DATA_VALID)
|
||||
|
||||
private val stringWriter = StringWriter()
|
||||
private val printWriter = PrintWriter(stringWriter)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTableLogger_header() {
|
||||
logger.printTableData(printWriter)
|
||||
val lines = logLines(stringWriter)
|
||||
|
||||
val line1 = lines[0]
|
||||
|
||||
assertEquals("table logger header is incorrect",
|
||||
HEADER_PREFIX + TEST_SECTION_NAME, line1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTableLogger_version() {
|
||||
logger.printTableData(printWriter)
|
||||
val lines = logLines(stringWriter)
|
||||
|
||||
val line2 = lines[1]
|
||||
|
||||
assertEquals("version probably shouldn't have changed",
|
||||
"version $VERSION", line2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTableLogger_footer() {
|
||||
logger.printTableData(printWriter)
|
||||
val lines = logLines(stringWriter)
|
||||
|
||||
val footer = lines.last()
|
||||
android.util.Log.d("evanevan", footer)
|
||||
android.util.Log.d("evanevan", lines.toString())
|
||||
|
||||
assertEquals("table logger footer is incorrect",
|
||||
FOOTER_PREFIX + TEST_SECTION_NAME, footer)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTableLogger_data_length() {
|
||||
logger.printTableData(printWriter)
|
||||
val lines = logLines(stringWriter)
|
||||
|
||||
// Header is 2 lines long, plus a line for the column defs so data is lines[3..last()-1]
|
||||
val data = lines.subList(3, lines.size - 1)
|
||||
assertEquals(TEST_DATA_LENGTH, data.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTableLogger_data_columns() {
|
||||
logger.printTableData(printWriter)
|
||||
val lines = logLines(stringWriter)
|
||||
|
||||
// Header is always 2 lines long so data is lines[2..last()-1]
|
||||
val data = lines.subList(3, lines.size - 1)
|
||||
|
||||
data.forEach { dataLine ->
|
||||
assertEquals(TEST_COLUMNS.size, dataLine.split(SEPARATOR).size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInvalidLinesAreFiltered() {
|
||||
// GIVEN an invalid data row, by virtue of having an extra field
|
||||
val invalidLine = List(TEST_COLUMNS.size) { col ->
|
||||
"data${col}X"
|
||||
} + "INVALID COLUMN"
|
||||
val invalidData = TEST_DATA_VALID.toMutableList().also {
|
||||
it.add(invalidLine)
|
||||
}
|
||||
|
||||
// WHEN the table logger is created and asked to print the table
|
||||
val tableLogger = DumpsysTableLogger(
|
||||
TEST_SECTION_NAME,
|
||||
TEST_COLUMNS,
|
||||
invalidData)
|
||||
|
||||
tableLogger.printTableData(printWriter)
|
||||
|
||||
// THEN the invalid line is filtered out
|
||||
val invalidString = invalidLine.joinToString(separator = SEPARATOR)
|
||||
val logString = stringWriter.toString()
|
||||
|
||||
assertThat(logString).doesNotContain(invalidString)
|
||||
}
|
||||
|
||||
private fun logLines(sw: StringWriter): List<String> {
|
||||
return sw.toString().split("\n").filter { it.isNotBlank() }
|
||||
}
|
||||
}
|
||||
|
||||
// Copying these here from [DumpsysTableLogger] so that we catch any accidental versioning change
|
||||
private const val HEADER_PREFIX = "SystemUI TableSection START: "
|
||||
private const val FOOTER_PREFIX = "SystemUI TableSection END: "
|
||||
private const val SEPARATOR = "|" // TBD
|
||||
private const val VERSION = "1"
|
||||
|
||||
const val TEST_SECTION_NAME = "TestTableSection"
|
||||
const val TEST_DATA_LENGTH = 5
|
||||
val TEST_COLUMNS = arrayListOf("col1", "col2", "col3")
|
||||
val TEST_DATA_VALID = List(TEST_DATA_LENGTH) { row ->
|
||||
List(TEST_COLUMNS.size) { col ->
|
||||
"data$col$row"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user