1 /*
  2  * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @bug 8284161 8287008
 27  * @summary Basic test for jcmd Thread.dump_to_file
 28  * @modules jdk.jcmd
 29  * @library /test/lib
 30  * @run junit/othervm -Dminify=true ThreadDumpToFileTest
 31  * @run junit/othervm -Dminify=false ThreadDumpToFileTest
 32  */
 33 
 34 import java.io.IOException;
 35 import java.nio.file.Files;
 36 import java.nio.file.Path;
 37 import java.util.stream.Stream;
 38 import jdk.test.lib.dcmd.PidJcmdExecutor;
 39 import jdk.test.lib.process.OutputAnalyzer;
 40 import jdk.test.lib.threaddump.ThreadDump;
 41 
 42 import org.junit.jupiter.api.Test;
 43 import org.junit.jupiter.api.BeforeAll;
 44 import static org.junit.jupiter.api.Assertions.*;
 45 
 46 class ThreadDumpToFileTest {
 47     private static boolean minify;
 48 
 49     @BeforeAll
 50     static void setup() throws Exception {
 51         minify = Boolean.getBoolean("minify");
 52     }
 53 
 54     /**
 55      * Test thread dump, should be in plain text format.
 56      */
 57     @Test
 58     void testThreadDump() throws IOException {
 59         Path file = genThreadDumpPath(".txt");
 60         testPlainThreadDump(file);
 61     }
 62 
 63     /**
 64      * Test thread dump in plain text format.
 65      */
 66     @Test
 67     void testPlainThreadDump() throws IOException {
 68         Path file = genThreadDumpPath(".txt");
 69         testPlainThreadDump(file, "-format=plain");
 70     }
 71 
 72     /**
 73      * Test thread dump in JSON format.
 74      */
 75     @Test
 76     void testJsonThreadDump() throws IOException {
 77         Path file = genThreadDumpPath(".json");
 78         jcmdThreadDumpToFile(file, "-format=json")
 79                 .shouldMatch("Created");
 80 
 81         // parse the JSON text
 82         String jsonText = Files.readString(file);
 83         ThreadDump threadDump = ThreadDump.parse(jsonText);
 84 
 85         // test that the process id is this process
 86         assertTrue(threadDump.processId() == ProcessHandle.current().pid());
 87 
 88         // test that the current thread is in the root thread container
 89         var rootContainer = threadDump.rootThreadContainer();
 90         var tid = Thread.currentThread().threadId();
 91         rootContainer.findThread(tid).orElseThrow();
 92     }
 93 
 94     /**
 95      * Test that an existing file is not overwritten.
 96      */
 97     @Test
 98     void testDoNotOverwriteFile() throws IOException {
 99         Path file = genThreadDumpPath(".txt");
100         Files.writeString(file, "xxx");
101 
102         jcmdThreadDumpToFile(file, "")
103                 .shouldMatch("exists");
104 
105         // file should not be overridden
106         assertEquals("xxx", Files.readString(file));
107     }
108 
109     /**
110      * Test overwriting an existing file.
111      */
112     @Test
113     void testOverwriteFile() throws IOException {
114         Path file = genThreadDumpPath(".txt");
115         Files.writeString(file, "xxx");
116         jcmdThreadDumpToFile(file, "-overwrite")
117                 .shouldMatch("Created");
118     }
119 
120     /**
121      * Test output file cannot be created.
122      */
123     @Test
124     void testFileCreateFails() throws IOException {
125         Path badFile = Path.of(".").toAbsolutePath()
126                 .resolve("does-not-exist")
127                 .resolve("does-not-exist")
128                 .resolve("threads.bad");
129         jcmdThreadDumpToFile(badFile, "-format=plain")
130                 .shouldMatch("Failed");
131         jcmdThreadDumpToFile(badFile, "-format=json")
132                 .shouldMatch("Failed");
133     }
134 
135     /**
136      * Test thread dump in plain text format.
137      */
138     private void testPlainThreadDump(Path file, String... options) throws IOException {
139         jcmdThreadDumpToFile(file, options).shouldMatch("Created");
140 
141         // test that thread dump contains the name and id of the current thread
142         String name = Thread.currentThread().getName();
143         long tid = Thread.currentThread().threadId();
144         String expected = "#" + tid + " \"" + name + "\"";
145         assertTrue(find(file, expected), expected + " not found in " + file);
146     }
147 
148     /**
149      * Generate a file path with the given suffix to use for the thread dump.
150      */
151     private Path genThreadDumpPath(String suffix) throws IOException {
152         Path dir = Path.of(".").toAbsolutePath();
153         Path file = Files.createTempFile(dir, "threads-", suffix);
154         Files.delete(file);
155         return file;
156     }
157 
158     /**
159      * Launches jcmd Thread.dump_to_file to obtain a thread dump of this VM.
160      */
161     private OutputAnalyzer jcmdThreadDumpToFile(Path file, String... options) {
162         String cmd = "Thread.dump_to_file";
163         for (String option : options) {
164             cmd += " " + option;
165         }
166         if (minify) {
167             cmd += " -minify";
168         }
169         return new PidJcmdExecutor().execute(cmd + " " + file);
170     }
171 
172     /**
173      * Returns true if the given file contains a line with the string.
174      */
175     private boolean find(Path file, String text) throws IOException {
176         try (Stream<String> stream = Files.lines(file)) {
177             return  stream.anyMatch(line -> line.indexOf(text) >= 0);
178         }
179     }
180 }