Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6bee37da authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Refactor ProtoLogController for testability and add unit tests" into main

parents 9bcb8485 e40daed6
Loading
Loading
Loading
Loading
+81 −9
Original line number Diff line number Diff line
@@ -27,7 +27,9 @@ import com.android.internal.protolog.common.IProtoLogGroup;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Controller for managing ProtoLog state and core logic.
@@ -46,24 +48,35 @@ public class ProtoLogController {
    @GuardedBy("mInitLock")
    private final Set<IProtoLogGroup> mGroups = new HashSet<>();

    void registerLogGroupInProcess(@NonNull IProtoLogGroup... groups) {
    /**
     * Registers a log group in the process.
     * @param groups The groups to register.
     */
    public void registerLogGroupInProcess(@NonNull IProtoLogGroup... groups) {
        synchronized (mInitLock) {
            var newGroups = Arrays.stream(groups)
                    .filter(Objects::nonNull)
                    .filter(group -> !mGroups.contains(group))
                    .toArray(IProtoLogGroup[]::new);
            if (newGroups.length == 0) {
                    .collect(Collectors.toUnmodifiableSet());
            if (newGroups.isEmpty()) {
                return;
            }

            mGroups.addAll(Arrays.asList(newGroups));
            assertForCollisions(newGroups);

            mGroups.addAll(newGroups);

            if (mProtoLogInstance != null) {
                mProtoLogInstance.registerGroups(newGroups);
                mProtoLogInstance.registerGroups(newGroups.toArray(new IProtoLogGroup[0]));
            }
        }
    }

    void init(@NonNull IProtoLogGroup... groups) {
    /**
     * Initializes the ProtoLog instance.
     * @param groups The groups to register.
     */
    public void init(@NonNull IProtoLogGroup... groups) {
        registerLogGroupInProcess(groups);

        synchronized (mInitLock) {
@@ -74,8 +87,8 @@ public class ProtoLogController {
            // These tracing instances are only used when we cannot or do not preprocess the source
            // files to extract out the log strings. Otherwise, the trace calls are replaced with
            // calls directly to the generated tracing implementations.
            if (ProtoLog.logOnlyToLogcat()) {
                mProtoLogInstance = new LogcatOnlyProtoLogImpl();
            if (shouldLogOnlyToLogcat()) {
                mProtoLogInstance = createLogcatOnlyInstance();
            } else {
                var datasource = ProtoLog.getSharedSingleInstanceDataSource();

@@ -85,6 +98,12 @@ public class ProtoLogController {
        }
    }

    @Nullable
    @VisibleForTesting
    public IProtoLog getProtoLogInstance() {
        return mProtoLogInstance;
    }

    /**
     * Tear down the ProtoLog instance. This should probably only be called from testing.
     * Otherwise there is no reason to teardown a ProtoLogController as it should exist for the
@@ -103,8 +122,35 @@ public class ProtoLogController {
        }
    }

    @VisibleForTesting
    @NonNull
    private PerfettoProtoLogImpl createAndEnableNewPerfettoProtoLogImpl(
    public Set<IProtoLogGroup> getRegisteredGroups() {
        return Set.copyOf(mGroups);
    }

    /**
     * Decides if logging should only go to Logcat.
     * Protected for testability.
     */
    protected boolean shouldLogOnlyToLogcat() {
        return ProtoLog.logOnlyToLogcat();
    }

    /**
     * Creates an instance of LogcatOnlyProtoLogImpl.
     * Protected for testability.
     */
    @NonNull
    protected IProtoLog createLogcatOnlyInstance() {
        return new LogcatOnlyProtoLogImpl();
    }

    /**
     * Creates and enables a new PerfettoProtoLogImpl.
     * Protected for testability.
     */
    @NonNull
    protected PerfettoProtoLogImpl createAndEnableNewPerfettoProtoLogImpl(
            @NonNull ProtoLogDataSource datasource, @NonNull IProtoLogGroup[] groups) {
        try {
            var unprocessedPerfettoProtoLogImpl =
@@ -116,4 +162,30 @@ public class ProtoLogController {
            throw new RuntimeException("Failed to create PerfettoProtoLogImpl", e);
        }
    }

    private void assertForCollisions(Set<IProtoLogGroup> newGroups) {
        // Check for ID collisions within the new groups first
        Set<Integer> newIds = new HashSet<>();
        for (IProtoLogGroup group : newGroups) {
            if (group == null) {
                continue;
            }
            if (!newIds.add(group.getId())) {
                throw new RuntimeException("ProtoLog group ID collision for ID " + group.getId()
                        + " within the same registration call.");
            }
        }

        // Check for collisions with already registered groups
        for (IProtoLogGroup group : newGroups) {
            for (IProtoLogGroup existingGroup : mGroups) {
                if (existingGroup.getId() == group.getId() && !existingGroup.equals(group)) {
                    throw new RuntimeException("ProtoLog group ID collision for ID "
                            + group.getId() + ". Group " + group.name()
                            + " conflicts with already registered group "
                            + existingGroup.name());
                }
            }
        }
    }
}
+388 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.internal.protolog;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;

import android.annotation.NonNull;

import androidx.annotation.Nullable;

import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClientArgs;
import com.android.internal.protolog.common.ILogger;
import com.android.internal.protolog.common.IProtoLog;
import com.android.internal.protolog.common.IProtoLogGroup;
import com.android.internal.protolog.common.LogLevel;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@RunWith(JUnit4.class)
public class ProtoLogControllerTest {

    private TestableProtoLogController mController;
    private MockProtoLog mMockProtoLogInstance;

    private static final TestProtoLogGroup GROUP_1 =
            new TestProtoLogGroup("GROUP_1", 1, true);
    private static final TestProtoLogGroup GROUP_2 =
            new TestProtoLogGroup("GROUP_2", 2, true);
    private static final TestProtoLogGroup GROUP_1_COLLISION =
            new TestProtoLogGroup("GROUP_1_COLLISION", 1, true);
    private static final TestProtoLogGroup DISABLED_GROUP =
            new TestProtoLogGroup("DISABLED_GROUP", 3, false);

    @Before
    public void setUp() {
        mMockProtoLogInstance = new MockProtoLog();
        mController = new TestableProtoLogController(mMockProtoLogInstance);
    }

    @Test
    public void registerSingleLogGroup() {
        mController.registerLogGroupInProcess(GROUP_1);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1);
    }

    @Test
    public void registerMultipleLogGroups() {
        mController.registerLogGroupInProcess(GROUP_1, GROUP_2);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1, GROUP_2);
    }

    @Test
    public void registerDuplicateLogGroupsDeduplicated() {
        mController.registerLogGroupInProcess(GROUP_1);
        mController.registerLogGroupInProcess(GROUP_1);
        assertThat(mController.getRegisteredGroups()).hasSize(1);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1);
    }

    @Test
    public void registerDifferentLogGroupsTogetherWithCollidingIdsThrows() {
        RuntimeException ex =
                assertThrows(
                        RuntimeException.class,
                        () -> mController.registerLogGroupInProcess(GROUP_1, GROUP_1_COLLISION));
        assertThat(ex).hasMessageThat().contains("ProtoLog group ID collision for ID 1");
    }

    @Test
    public void registerDifferentLogGroupsWithCollidingIdsThrows() {
        mController.registerLogGroupInProcess(GROUP_1);
        RuntimeException ex =
                assertThrows(
                        RuntimeException.class,
                        () -> mController.registerLogGroupInProcess(GROUP_1_COLLISION));
        assertThat(ex).hasMessageThat().contains("ProtoLog group ID collision for ID 1");
        assertThat(ex)
                .hasMessageThat()
                .contains(
                        "Group GROUP_1_COLLISION conflicts with already registered group GROUP_1");
    }

    @Test
    public void registerLogGroupInProcessNullGroupIsIgnored() {
        mController.registerLogGroupInProcess(GROUP_1, null, GROUP_2);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1, GROUP_2);
    }

    @Test
    public void registerLogGroupInProcessAfterInitCallsRegisterGroupsOnInstance() {
        mController.init(GROUP_1);
        mMockProtoLogInstance.clearRegisteredGroupsHistory();

        mController.registerLogGroupInProcess(GROUP_2);
        assertThat(mMockProtoLogInstance.getLastRegisteredGroups()).containsExactly(GROUP_2);
    }

    @Test
    public void initRegistersInitialGroups() {
        mController.init(GROUP_1, GROUP_2);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1, GROUP_2);
    }

    @Test
    public void initMultipleCallsAccumulatesGroups() {
        mController.init(GROUP_1);
        IProtoLog firstInstance = mController.getProtoLogInstance();
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1);

        mController.init(GROUP_2);
        assertThat(mController.getProtoLogInstance()).isSameInstanceAs(firstInstance);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1, GROUP_2);
    }

    @Test
    public void initCollisionInGroupsThrows() {
        RuntimeException ex =
                assertThrows(
                        RuntimeException.class,
                        () -> mController.init(GROUP_1, GROUP_1_COLLISION));
        assertThat(ex).hasMessageThat().contains("ProtoLog group ID collision for ID 1");
    }


    @Test
    public void getProtoLogInstanceBeforeInitReturnsNull() {
        ProtoLogController freshController = new ProtoLogController();
        assertNull(freshController.getProtoLogInstance());
    }

    @Test
    public void getRegisteredGroupsIsInitiallyEmpty() {
        ProtoLogController freshController = new ProtoLogController();
        assertThat(freshController.getRegisteredGroups()).isEmpty();
    }

    @Test
    public void getRegisteredGroupsAfterRegistrationContainsAllGroups() {
        mController.registerLogGroupInProcess(GROUP_1, GROUP_2);
        assertThat(mController.getRegisteredGroups()).containsExactly(GROUP_1, GROUP_2);
    }

    @Test
    public void getRegisteredGroupsReturnsUnmodifiableSet() {
        mController.registerLogGroupInProcess(GROUP_1);
        Set<IProtoLogGroup> groups = mController.getRegisteredGroups();
        assertThrows(UnsupportedOperationException.class, () -> groups.add(GROUP_2));
    }

    // Test Helpers

    static class TestableProtoLogController extends ProtoLogController {
        private final MockProtoLog mMockInjectedProtoLogInstance;

        TestableProtoLogController(MockProtoLog mockInjectedInstance) {
            super();
            this.mMockInjectedProtoLogInstance = mockInjectedInstance;
        }

        @Override
        @NonNull
        protected IProtoLog createLogcatOnlyInstance() {
            mMockInjectedProtoLogInstance.setInitialGroups(new HashSet<>(getRegisteredGroups()));
            return mMockInjectedProtoLogInstance;
        }

        @Override
        @NonNull
        protected PerfettoProtoLogImpl createAndEnableNewPerfettoProtoLogImpl(
                @NonNull ProtoLogDataSource datasource, @NonNull IProtoLogGroup[] currentGroups) {
            mMockInjectedProtoLogInstance.setInitialGroups(
                    new HashSet<>(Arrays.asList(currentGroups)));
            return new DummyPerfettoProtoLogImpl(datasource, currentGroups,
                    mMockInjectedProtoLogInstance);
        }
    }

    static class MockProtoLog implements IProtoLog {
        private final List<IProtoLogGroup> mLastRegisteredGroups = new ArrayList<>();
        private final Set<IProtoLogGroup> mInitialGroups = new HashSet<>();

        public void setInitialGroups(Set<IProtoLogGroup> groups) {
            this.mInitialGroups.clear();
            this.mInitialGroups.addAll(groups);
        }

        @Override
        public void log(@NonNull LogLevel logLevel, @NonNull IProtoLogGroup group, long messageHash,
                int paramsMask, @Nullable Object[] args) {
            // No-op for testing
        }

        @Override
        public void log(@NonNull LogLevel level, @NonNull IProtoLogGroup group,
                @NonNull String messageString, @NonNull Object[] args) {
            // No-op for testing
        }

        @Override
        public boolean isProtoEnabled() {
            return false;
        }

        @Override
        public int startLoggingToLogcat(@NonNull String[] groups, @NonNull ILogger logger) {
            return 0;
        }

        @Override
        public int stopLoggingToLogcat(@NonNull String[] groups, @NonNull ILogger logger) {
            return 0;
        }

        @Override
        public boolean isEnabled(@NonNull IProtoLogGroup group, @NonNull LogLevel level) {
            return group.isEnabled();
        }

        @Override
        public void registerGroups(@NonNull IProtoLogGroup[] groups) {
            mLastRegisteredGroups.clear();
            Collections.addAll(mLastRegisteredGroups, groups);
        }

        @Override
        @NonNull
        public List<IProtoLogGroup> getRegisteredGroups() {
            return List.copyOf(mInitialGroups);
        }

        public List<IProtoLogGroup> getLastRegisteredGroups() {
            return List.copyOf(mLastRegisteredGroups);
        }

        public void clearRegisteredGroupsHistory() {
            mLastRegisteredGroups.clear();
        }
    }

    static class DummyPerfettoProtoLogImpl extends PerfettoProtoLogImpl {
        private final MockProtoLog mWrappedInstance;

        DummyPerfettoProtoLogImpl(ProtoLogDataSource dataSource, IProtoLogGroup[] groups,
                MockProtoLog wrappedInstance) {
            super(dataSource, protoLogInstance -> {}, groups);
            this.mWrappedInstance = wrappedInstance;
            this.mWrappedInstance.registerGroups(groups);
        }

        public MockProtoLog getWrappedInstance() {
            return mWrappedInstance;
        }

        @Override
        public void enable() {
            // No-op for tests
        }

        @Override
        public void log(@NonNull LogLevel level, @NonNull IProtoLogGroup group,
                @NonNull String messageString, @NonNull Object[] args) {
            mWrappedInstance.log(level, group, messageString, args);
        }

        @Override
        public boolean isEnabled(@NonNull IProtoLogGroup group, @NonNull LogLevel level) {
            return mWrappedInstance.isEnabled(group, level);
        }

        @Override
        public void registerGroups(@NonNull IProtoLogGroup[] groups) {
            mWrappedInstance.registerGroups(groups);
        }

        @NonNull
        @Override
        protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() {
            return new RegisterClientArgs();
        }

        @Override
        @NonNull
        public List<IProtoLogGroup> getRegisteredGroups() {
            return mWrappedInstance.getRegisteredGroups();
        }

        @Override
        void dumpViewerConfig() {
            // No-op for testing
        }

        @NonNull
        @Override
        String getLogcatMessageString(@NonNull Message message) {
            return "";
        }
    }

    static class TestProtoLogGroup implements IProtoLogGroup {
        private final String mName;
        private final int mId;
        private final boolean mEnabled;
        private boolean mLogToProto = true;
        private boolean mLogToLogcat = true;

        TestProtoLogGroup(String name, int id, boolean enabled) {
            this.mName = name;
            this.mId = id;
            this.mEnabled = enabled;
        }

        @Override public String name() {
            return mName;
        }

        @Override public int getId() {
            return mId;
        }

        @Override public boolean isEnabled() {
            return mEnabled;
        }

        @Override public String getTag() {
            return mName;
        }

        @Override public boolean isLogToProto() {
            return mLogToProto;
        }

        @Override public void setLogToProto(boolean val) {
            this.mLogToProto = val;
        }

        @Override public boolean isLogToLogcat() {
            return mLogToLogcat;
        }

        @Override public void setLogToLogcat(boolean val) {
            this.mLogToLogcat = val;
        }

        @Override public boolean isLogToAny() {
            return isLogToLogcat() || isLogToProto();
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            TestProtoLogGroup that = (TestProtoLogGroup) o;
            return mId == that.mId && mName.equals(that.mName);
        }

        @Override
        public int hashCode() {
            return 31 * mName.hashCode() + mId;
        }
    }
}