This is an automated email from the ASF dual-hosted git repository.

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 3cd2aa4a6bb [fix](profile) Harden the profile completion state against 
lost reports and concurrent rendering (#69044)
3cd2aa4a6bb is described below

commit 3cd2aa4a6bbe458f76594b98cb978a821446653d
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Oct 19 07:34:42 2026 +0100

    [fix](profile) Harden the profile completion state against lost reports and 
concurrent rendering (#69044)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66227, #64392
    
    Problem Summary:
    
    **In short.** A query profile shows a completion state (RUNNING,
    COLLECTING, COMPLETE, INCOMPLETE), and regression tests wait for
    COMPLETE before they read profile counters. On master the state can be
    wrong in three ways: a profile whose BE reports were dropped after a
    failed spill says COMPLETE, a profile rendered while it is being spilled
    can keep COLLECTING for good, and a profile can say COMPLETE before the
    last BE report is visible in its per-pipeline section. This PR ports the
    fixes for all three from the profile part of branch-4.1 #66227.
    
    **Background.** #64392 added the completion state on master.
    `ExecutionProfile` counts the BEs that sent a final report for each
    fragment, and `Profile` derives the state from that: RUNNING until the
    query finishes, then COLLECTING until every BE has sent its final
    report, then COMPLETE. When `ProfileManager` spills a profile to disk it
    writes a terminal state instead (COMPLETE, or INCOMPLETE when reports
    are still missing), and after the spill it releases the in-memory
    execution profiles. The state is printed in the profile summary
    (`Profile Completion State: ...`) and in the query profile list.
    branch-4.1 got the same mechanism inside #66227, together with the three
    fixes below; master never got the fixes.
    
    **The problem, and what it cost.**
    
    | Case | master | with this PR |
    |---|---|---|
    | The spill fails (for example the profile directory is not writable).
    `writeProfileToStorage` still releases the reports, and the next spill
    writes the profile again. | The state is computed over an empty report
    list, so it shows COMPLETE, and the next spill stores COMPLETE for a
    profile whose BE reports are gone. | INCOMPLETE, in memory and in the
    stored profile. |
    | The profile is rendered (`SHOW QUERY PROFILE`, the web UI) while it is
    being spilled. Rendering writes the display state into the same summary
    field the spill has just set to the terminal state. | Rendered before
    serialization: the stored file says COLLECTING. Rendered after
    serialization but before the storage path is set: the file says
    INCOMPLETE, but the profile in memory says COLLECTING from then on.
    Either way a poller waiting for a terminal state never gets one. | The
    terminal state is frozen before serialization and returned from then on;
    a failed write drops the frozen state so a retry recomputes it. |
    | A BE sends its final report. `updateProfile` marks the BE done under
    the lock and attaches its pipeline nodes after releasing the lock. | A
    reader that sees COMPLETE in between can render, or spill, a profile
    whose per-pipeline section lacks that report while the merged section
    already has it. | The nodes are attached under the same lock, before the
    BE is marked done. |
    
    **How this PR fixes it.**
    
    - `Profile.releaseMemory()` records that the reports were released; from
    then on both the display state and the storage state are INCOMPLETE.
    - `Profile.writeToStorage()` freezes the storage state before it
    serializes the summary, and `getProfileCompletionState()` returns the
    frozen state first. The two state updates share a dedicated lock, and a
    failed write clears the frozen state.
    - `ExecutionProfile.updateMultiBeProfile()` attaches the report's
    pipeline nodes to the fragment profile inside the write lock, before it
    adds the BE to the done set.
    
    The code is the same as on branch-4.1 and branch-4.2, except for that
    lock: branch-4.x makes the two updates `synchronized` on the `Profile`
    monitor, which deadlocks. `ProfileManager` renders a profile under its
    read lock and rendering then waits for the monitor, while
    `Profile.updateSummary()` holds the monitor and waits for the manager's
    write lock in `pushProfile()`.
    
    **Results.** `ProfileCompletionStateTest` (new) and
    `ProfileManagerTest#testFailedSpillRemainsIncompleteAfterMemoryRelease`
    (new) cover the failed spill, a failed write followed by a successful
    one, rendering paused before and after serialization, the state after
    reports are released, multiple pipeline nodes from one BE, and a stale
    non-final report after the final one.
    `renderingDoesNotWaitForProfileMonitor` renders a profile while the test
    holds the `Profile` monitor; with the monitor-based updates of
    branch-4.x it times out. With the fix, all 36 tests in
    `ProfileCompletionStateTest` and `ProfileManagerTest` pass. With
    master's `Profile` and `ExecutionProfile`,
    `testFailedSpillRemainsIncompleteAfterMemoryRelease` gets COMPLETE and
    `renderingDuringStoragePreservesTerminalState` gets COLLECTING where
    both expect INCOMPLETE; removing only the reset after a failed write
    makes `failedStorageDoesNotFreezeCompletionState` get INCOMPLETE where
    it expects COLLECTING. The order in which `ExecutionProfile` publishes
    the nodes is a race and has no deterministic test.
    
    Co-authored-by: Fangyuan Deng <[email protected]>
---
 .../doris/common/profile/ExecutionProfile.java     |  11 +-
 .../org/apache/doris/common/profile/Profile.java   |  34 ++-
 .../common/profile/ProfileCompletionStateTest.java | 244 +++++++++++++++++++++
 .../doris/common/profile/ProfileManagerTest.java   |  29 +++
 4 files changed, 311 insertions(+), 7 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/ExecutionProfile.java
 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/ExecutionProfile.java
index 067c695e1d8..48834936821 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/ExecutionProfile.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/ExecutionProfile.java
@@ -168,13 +168,17 @@ public class ExecutionProfile {
     }
 
     private boolean updateMultiBeProfile(int fragmentId, TNetworkAddress 
backendHBAddress,
-            List<RuntimeProfile> taskProfile, boolean isDone) {
+            List<RuntimeProfile> taskProfile, List<RuntimeProfile> 
profileNodes, boolean isDone) {
         multiBeProfileLock.writeLock().lock();
         try {
             if (!isDone && 
fragmentIdDoneBackends.get(fragmentId).contains(backendHBAddress)) {
                 return false;
             }
             multiBeProfile.get(fragmentId).put(backendHBAddress, taskProfile);
+            // Publish both rendered forms before completion becomes visible 
to profile readers.
+            for (RuntimeProfile profileNode : profileNodes) {
+                fragmentProfiles.get(fragmentId).addChild(profileNode, true);
+            }
             if (isDone) {
                 fragmentIdDoneBackends.get(fragmentId).add(backendHBAddress);
             }
@@ -303,12 +307,9 @@ public class ExecutionProfile {
                 profileNode.setIsDone(isDone);
                 profileNodes.add(profileNode);
             }
-            if (!updateMultiBeProfile(fragmentId, backendHBAddress, 
taskProfile, isDone)) {
+            if (!updateMultiBeProfile(fragmentId, backendHBAddress, 
taskProfile, profileNodes, isDone)) {
                 continue;
             }
-            for (RuntimeProfile profileNode : profileNodes) {
-                fragmentProfiles.get(fragmentId).addChild(profileNode, true);
-            }
         }
 
         LOG.info("Profile update finished query: {} fragments: {} isDone: {}",
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/profile/Profile.java 
b/fe/fe-core/src/main/java/org/apache/doris/common/profile/Profile.java
index b6da0447c31..efc695e3518 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/common/profile/Profile.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/profile/Profile.java
@@ -117,6 +117,12 @@ public class Profile {
     // 1. profile is stored to storage
     // 2. or profile is loaded from storage
     private String profileStoragePath = "";
+    private volatile String storageProfileCompletionState;
+    private volatile boolean executionProfilesReleased;
+    // Serializes the storage and display updates of the completion state. Not 
the Profile monitor:
+    // ProfileManager renders under its read lock, while updateSummary() holds 
the monitor and then
+    // takes the manager's write lock.
+    private final Object completionStateLock = new Object();
     // isQueryFinished means the coordinator or stmt executor is finished.
     // does not mean the profile report has finished, since the report is 
async.
     // finish of collection of profile is marked by isCompleted of 
ExecutionProfiles.
@@ -565,6 +571,9 @@ public class Profile {
     }
 
     public void releaseMemory() {
+        // The manager also releases reports after a failed spill. An empty 
list then means
+        // lost reports, not successful collection; retain that distinction 
across retries.
+        executionProfilesReleased = true;
         this.executionProfiles.clear();
         this.changedSessionVarCache = "";
         this.physicalPlan = null;
@@ -627,7 +636,13 @@ public class Profile {
         return !Strings.isNullOrEmpty(profileStoragePath);
     }
 
+    // Query completion precedes asynchronous BE reports; persisted profiles 
must retain
+    // their terminal state even after the in-memory reports have been 
released.
     public String getProfileCompletionState() {
+        String storageState = storageProfileCompletionState;
+        if (storageState != null) {
+            return storageState;
+        }
         if (profileHasBeenStored()) {
             String storedState = 
summaryProfile.getSummary().getInfoString(SummaryProfile.PROFILE_COMPLETION_STATE);
             if (!Strings.isNullOrEmpty(storedState)) {
@@ -636,6 +651,10 @@ public class Profile {
             return SummaryProfile.PROFILE_COMPLETION_STATE_UNKNOWN;
         }
 
+        if (executionProfilesReleased) {
+            return SummaryProfile.PROFILE_COMPLETION_STATE_INCOMPLETE;
+        }
+
         if (!isQueryFinished) {
             return SummaryProfile.PROFILE_COMPLETION_STATE_RUNNING;
         }
@@ -650,6 +669,9 @@ public class Profile {
     }
 
     private String getProfileCompletionStateForStorage() {
+        if (executionProfilesReleased) {
+            return SummaryProfile.PROFILE_COMPLETION_STATE_INCOMPLETE;
+        }
         if (!isQueryFinished) {
             return SummaryProfile.PROFILE_COMPLETION_STATE_RUNNING;
         }
@@ -664,11 +686,18 @@ public class Profile {
     }
 
     private void updateProfileCompletionStateForStorage() {
-        
summaryProfile.setProfileCompletionState(getProfileCompletionStateForStorage());
+        synchronized (completionStateLock) {
+            // Freeze the terminal state before serialization. Concurrent 
rendering must not
+            // replace INCOMPLETE with COLLECTING while the storage path is 
still unpublished.
+            storageProfileCompletionState = 
getProfileCompletionStateForStorage();
+            
summaryProfile.setProfileCompletionState(storageProfileCompletionState);
+        }
     }
 
     private void updateProfileCompletionStateForDisplay() {
-        summaryProfile.setProfileCompletionState(getProfileCompletionState());
+        synchronized (completionStateLock) {
+            
summaryProfile.setProfileCompletionState(getProfileCompletionState());
+        }
     }
 
     // Profile IO threads races with Coordinator threads.
@@ -741,6 +770,7 @@ public class Profile {
             this.profileStoragePath = profileFilePath;
 
         } catch (Exception e) {
+            storageProfileCompletionState = null;
             LOG.error("write {} summary profile failed", getId(), e);
             return;
         } finally {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileCompletionStateTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileCompletionStateTest.java
new file mode 100644
index 00000000000..497e3e8f1d0
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileCompletionStateTest.java
@@ -0,0 +1,244 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 org.apache.doris.common.profile;
+
+import org.apache.doris.common.util.DebugUtil;
+import org.apache.doris.planner.PlanFragmentId;
+import org.apache.doris.thrift.TDetailedReportParams;
+import org.apache.doris.thrift.TNetworkAddress;
+import org.apache.doris.thrift.TQueryProfile;
+import org.apache.doris.thrift.TRuntimeProfileNode;
+import org.apache.doris.thrift.TRuntimeProfileTree;
+import org.apache.doris.thrift.TUniqueId;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+class ProfileCompletionStateTest {
+    @TempDir
+    Path temporary;
+
+    private String newFolder() throws IOException {
+        return Files.createTempDirectory(temporary, "profile_").toString();
+    }
+
+    private Profile profile() {
+        UUID id = UUID.randomUUID();
+        TUniqueId queryId = new TUniqueId(id.getMostSignificantBits(), 
id.getLeastSignificantBits());
+        Profile profile = new Profile();
+        
profile.getSummaryProfile().getSummary().addInfoString(SummaryProfile.PROFILE_ID,
 DebugUtil.printId(queryId));
+        profile.addExecutionProfile(new ExecutionProfile(queryId, 
Collections.singletonList(0)));
+        return profile;
+    }
+
+    private void report(ExecutionProfile execution, String host, boolean done) 
{
+        TRuntimeProfileNode node = new TRuntimeProfileNode();
+        node.setName("PipelineProfile");
+        node.setNumChildren(0);
+        node.setCounters(new ArrayList<>());
+        node.setMetadata(0);
+        node.setIndent(false);
+        node.setInfoStrings(new HashMap<>());
+        node.setInfoStringsDisplayOrder(new ArrayList<>());
+        node.setChildCountersMap(new HashMap<>());
+        node.setTimestamp(0);
+        TDetailedReportParams params = new TDetailedReportParams();
+        params.setProfile(new 
TRuntimeProfileTree(Collections.singletonList(node)));
+        params.setIsFragmentLevel(false);
+        TQueryProfile report = new TQueryProfile();
+        report.setQueryId(execution.getQueryId());
+        // Multiple pipeline nodes from one BE must not count as multiple BE 
reports.
+        report.putToFragmentIdToProfile(0, Arrays.asList(params, params));
+        execution.updateProfile(report, new TNetworkAddress(host, 9050), done);
+    }
+
+    @Test
+    void waitsForEveryBackendAndRejectsStaleReports() {
+        Profile profile = profile();
+        ExecutionProfile execution = profile.getExecutionProfiles().get(0);
+        execution.addFragmentBackend(new PlanFragmentId(0), 1L);
+        execution.addFragmentBackend(new PlanFragmentId(0), 2L);
+        Assertions.assertEquals("RUNNING", 
profile.getProfileCompletionState());
+        profile.markQueryFinished();
+        Assertions.assertEquals("COLLECTING", 
profile.getProfileCompletionState());
+        report(execution, "127.0.0.1", true);
+        Assertions.assertEquals("COLLECTING", 
profile.getProfileCompletionState());
+        report(execution, "127.0.0.2", true);
+        Assertions.assertEquals("COMPLETE", 
profile.getProfileCompletionState());
+        report(execution, "127.0.0.1", false);
+        Assertions.assertEquals("COMPLETE", 
profile.getProfileCompletionState());
+        Assertions.assertTrue(profile.getProfileByLevel().contains("Profile 
Completion State: COMPLETE"));
+    }
+
+    private static class PausingSummaryProfile extends SummaryProfile {
+        private final transient boolean beforeSerialization;
+        private final transient CountDownLatch storagePaused;
+        private final transient CountDownLatch resumeStorage;
+
+        private PausingSummaryProfile(boolean beforeSerialization, 
CountDownLatch storagePaused,
+                CountDownLatch resumeStorage) {
+            this.beforeSerialization = beforeSerialization;
+            this.storagePaused = storagePaused;
+            this.resumeStorage = resumeStorage;
+        }
+
+        @Override
+        public void write(DataOutput output) throws IOException {
+            if (!beforeSerialization) {
+                super.write(output);
+            }
+            storagePaused.countDown();
+            try {
+                if (!resumeStorage.await(10, TimeUnit.SECONDS)) {
+                    throw new IOException("Timed out waiting for concurrent 
profile rendering");
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new IOException(e);
+            }
+            if (beforeSerialization) {
+                super.write(output);
+            }
+        }
+    }
+
+    private static class FailingSummaryProfile extends SummaryProfile {
+        private transient boolean fail = true;
+
+        @Override
+        public void write(DataOutput output) throws IOException {
+            if (fail) {
+                fail = false;
+                throw new IOException("Injected storage failure");
+            }
+            super.write(output);
+        }
+    }
+
+    @Test
+    void failedStorageDoesNotFreezeCompletionState() throws Exception {
+        Profile profile = profile();
+        SummaryProfile summary = new FailingSummaryProfile();
+        summary.getSummary().addInfoString(SummaryProfile.PROFILE_ID, 
profile.getId());
+        profile.setSummaryProfile(summary);
+        ExecutionProfile execution = profile.getExecutionProfiles().get(0);
+        execution.addFragmentBackend(new PlanFragmentId(0), 1L);
+        profile.markQueryFinished();
+        String directory = newFolder();
+        profile.writeToStorage(directory);
+        Assertions.assertFalse(profile.profileHasBeenStored());
+        Assertions.assertEquals("COLLECTING", 
profile.getProfileCompletionState());
+        report(execution, "127.0.0.1", true);
+        profile.writeToStorage(directory);
+        Assertions.assertTrue(profile.profileHasBeenStored());
+        Assertions.assertEquals("COMPLETE", 
Profile.read(profile.getProfileStoragePath()).getProfileCompletionState());
+    }
+
+    @Test
+    void renderingDuringStoragePreservesTerminalState() throws Exception {
+        for (boolean beforeSerialization : new boolean[] {true, false}) {
+            Profile profile = profile();
+            CountDownLatch storagePaused = new CountDownLatch(1);
+            CountDownLatch resumeStorage = new CountDownLatch(1);
+            SummaryProfile summary = new 
PausingSummaryProfile(beforeSerialization, storagePaused, resumeStorage);
+            summary.getSummary().addInfoString(SummaryProfile.PROFILE_ID, 
profile.getId());
+            profile.setSummaryProfile(summary);
+            profile.getExecutionProfiles().get(0).addFragmentBackend(new 
PlanFragmentId(0), 1L);
+            profile.markQueryFinished();
+            String directory = newFolder();
+            ExecutorService writer = Executors.newSingleThreadExecutor();
+            try {
+                Future<?> stored = writer.submit(() -> 
profile.writeToStorage(directory));
+                Assertions.assertTrue(storagePaused.await(10, 
TimeUnit.SECONDS));
+                // Render both before serialization and before path 
publication, while storage is paused.
+                profile.getProfileByLevel();
+                resumeStorage.countDown();
+                stored.get(10, TimeUnit.SECONDS);
+                Assertions.assertTrue(profile.profileHasBeenStored());
+                Assertions.assertEquals("INCOMPLETE", 
profile.getProfileCompletionState());
+                Profile restored = 
Profile.read(profile.getProfileStoragePath());
+                Assertions.assertNotNull(restored);
+                Assertions.assertEquals("INCOMPLETE", 
restored.getProfileCompletionState());
+            } finally {
+                resumeStorage.countDown();
+                writer.shutdownNow();
+                Assertions.assertTrue(writer.awaitTermination(10, 
TimeUnit.SECONDS));
+            }
+        }
+    }
+
+    @Test
+    void renderingDoesNotWaitForProfileMonitor() throws Exception {
+        Profile profile = profile();
+        ExecutionProfile execution = profile.getExecutionProfiles().get(0);
+        execution.addFragmentBackend(new PlanFragmentId(0), 1L);
+        profile.markQueryFinished();
+        report(execution, "127.0.0.1", true);
+        ExecutorService renderer = Executors.newSingleThreadExecutor();
+        try {
+            // updateSummary() holds this monitor while it takes 
ProfileManager's write lock, and
+            // ProfileManager renders under its read lock, so rendering must 
not need the monitor.
+            synchronized (profile) {
+                Future<String> rendered = 
renderer.submit(profile::getProfileByLevel);
+                Assertions.assertTrue(rendered.get(10, TimeUnit.SECONDS)
+                        .contains("Profile Completion State: COMPLETE"));
+            }
+        } finally {
+            renderer.shutdownNow();
+            Assertions.assertTrue(renderer.awaitTermination(10, 
TimeUnit.SECONDS));
+        }
+    }
+
+    @Test
+    void persistsTerminalStateBeforeReleasingReports() throws Exception {
+        for (boolean complete : new boolean[] {false, true}) {
+            Profile profile = profile();
+            ExecutionProfile execution = profile.getExecutionProfiles().get(0);
+            execution.addFragmentBackend(new PlanFragmentId(0), 1L);
+            profile.markQueryFinished();
+            if (complete) {
+                report(execution, "127.0.0.1", true);
+            }
+            profile.writeToStorage(newFolder());
+            Assertions.assertTrue(profile.profileHasBeenStored());
+            profile.releaseMemory();
+            String expected = complete ? "COMPLETE" : "INCOMPLETE";
+            Assertions.assertEquals(expected, 
profile.getProfileCompletionState());
+            Profile restored = Profile.read(profile.getProfileStoragePath());
+            Assertions.assertNotNull(restored);
+            Assertions.assertEquals(expected, 
restored.getProfileCompletionState());
+            
Assertions.assertTrue(restored.getProfileByLevel().contains("Profile Completion 
State: " + expected));
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
index 75a7c39703c..38d1a4deb5d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/profile/ProfileManagerTest.java
@@ -571,6 +571,35 @@ class ProfileManagerTest {
         Assertions.assertEquals(30 / 2, profiles.size());
     }
 
+    @Test
+    void testFailedSpillRemainsIncompleteAfterMemoryRelease() throws Exception 
{
+        UUID id = UUID.randomUUID();
+        TUniqueId queryId = new TUniqueId(id.getMostSignificantBits(), 
id.getLeastSignificantBits());
+        Profile profile = constructProfile(DebugUtil.printId(queryId));
+        ExecutionProfile execution = new ExecutionProfile(queryId, 
Lists.newArrayList(0));
+        execution.addFragmentBackend(0, 1L);
+        profile.addExecutionProfile(execution);
+        profile.markQueryFinished();
+        profile.setQueryFinishTimestamp(0);
+        profileManager.pushProfile(profile);
+        profileManager.addExecutionProfile(execution);
+        File invalidDirectory = new File(tempDir, "not_a_directory");
+        Assertions.assertTrue(invalidDirectory.createNewFile());
+        ProfileManager.PROFILE_STORAGE_PATH = 
invalidDirectory.getAbsolutePath();
+
+        profileManager.writeProfileToStorage();
+
+        Assertions.assertFalse(profile.profileHasBeenStored());
+        Assertions.assertTrue(profile.getExecutionProfiles().isEmpty());
+        Assertions.assertEquals("INCOMPLETE", 
profile.getProfileCompletionState());
+        Assertions.assertTrue(profile.getProfileByLevel().contains("Profile 
Completion State: INCOMPLETE"));
+        // A later successful spill cannot recreate the reports discarded 
after the failed write.
+        ProfileManager.PROFILE_STORAGE_PATH = tempDir.getAbsolutePath();
+        profileManager.writeProfileToStorage();
+        Assertions.assertTrue(profile.profileHasBeenStored());
+        Assertions.assertEquals("INCOMPLETE", 
Profile.read(profile.getProfileStoragePath()).getProfileCompletionState());
+    }
+
     @Test
     void testWriteProfileToStorage() throws InterruptedException {
         List<Profile> spyProfiles = new ArrayList<>();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to