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 ThreadDumpToFileTest
 31  */
 32 
 33 import java.io.IOException;
 34 import java.nio.file.Files;
 35 import java.nio.file.Path;
 36 import java.util.stream.Stream;
 37 import jdk.test.lib.dcmd.PidJcmdExecutor;
 38 import jdk.test.lib.process.OutputAnalyzer;
 39 import jdk.test.lib.threaddump.ThreadDump;
 40 
 41 import org.junit.jupiter.api.Test;
 42 import static org.junit.jupiter.api.Assertions.*;
 43 
 44 class ThreadDumpToFileTest {
 45 
 46     /**
 47      * Test thread dump, should be in plain text format.
 48      */
 49     @Test
 50     void testThreadDump() throws IOException {
 51         Path file = genThreadDumpPath(".txt");
 52         testPlainThreadDump(file);
 53     }
 54 
 55     /**
 56      * Test thread dump in plain text format.
 57      */
 58     @Test
 59     void testPlainThreadDump() throws IOException {
 60         Path file = genThreadDumpPath(".txt");
 61         testPlainThreadDump(file, "-format=plain");
 62     }
 63 
 64     /**
 65      * Test thread dump in JSON format.
 66      */
 67     @Test
 68     void testJsonThreadDump() throws IOException {
 69         Path file = genThreadDumpPath(".json");
 70         jcmdThreadDumpToFile(file, "-format=json")
 71                 .shouldMatch("Created");
 72 
 73         // parse the JSON text
 74         String jsonText = Files.readString(file);
 75         ThreadDump threadDump = ThreadDump.parse(jsonText);
 76 
 77         // test that the process id is this process
 78         assertTrue(threadDump.processId() == ProcessHandle.current().pid());
 79 
 80         // test that the current thread is in the root thread container
 81         var rootContainer = threadDump.rootThreadContainer();
 82         var tid = Thread.currentThread().threadId();
 83         rootContainer.findThread(tid).orElseThrow();
 84     }
 85 
 86     /**
 87      * Test that an existing file is not overwritten.
 88      */
 89     @Test
 90     void testDoNotOverwriteFile() throws IOException {
 91         Path file = genThreadDumpPath(".txt");
 92         Files.writeString(file, "xxx");
 93 
 94         jcmdThreadDumpToFile(file, "")
 95                 .shouldMatch("exists");
 96 
 97         // file should not be overridden
 98         assertEquals("xxx", Files.readString(file));
 99     }
100 
101     /**
102      * Test overwriting an existing file.
103      */
104     @Test
105     void testOverwriteFile() throws IOException {
106         Path file = genThreadDumpPath(".txt");
107         Files.writeString(file, "xxx");
108         jcmdThreadDumpToFile(file, "-overwrite")
109                 .shouldMatch("Created");
110     }
111 
112     /**
113      * Test output file cannot be created.
114      */
115     @Test
116     void testFileCreateFails() throws IOException {
117         Path badFile = Path.of(".").toAbsolutePath()
118                 .resolve("does-not-exist")
119                 .resolve("does-not-exist")
120                 .resolve("threads.bad");
121         jcmdThreadDumpToFile(badFile, "-format=plain")
122                 .shouldMatch("Failed");
123         jcmdThreadDumpToFile(badFile, "-format=json")
124                 .shouldMatch("Failed");
125     }
126 
127     /**
128      * Test thread dump in plain text format.
129      */
130     private void testPlainThreadDump(Path file, String... options) throws IOException {
131         jcmdThreadDumpToFile(file, options).shouldMatch("Created");
132 
133         // test that thread dump contains the name and id of the current thread
134         String name = Thread.currentThread().getName();
135         long tid = Thread.currentThread().threadId();
136         String expected = "#" + tid + " \"" + name + "\"";
137         assertTrue(find(file, expected), expected + " not found in " + file);
138     }
139 
140     /**
141      * Generate a file path with the given suffix to use for the thread dump.
142      */
143     private Path genThreadDumpPath(String suffix) throws IOException {
144         Path dir = Path.of(".").toAbsolutePath();
145         Path file = Files.createTempFile(dir, "threads-", suffix);
146         Files.delete(file);
147         return file;
148     }
149 
150     /**
151      * Launches jcmd Thread.dump_to_file to obtain a thread dump of this VM.
152      */
153     private OutputAnalyzer jcmdThreadDumpToFile(Path file, String... options) {
154         String cmd = "Thread.dump_to_file";
155         for (String option : options) {
156             cmd += " " + option;
157         }
158         return new PidJcmdExecutor().execute(cmd + " " + file);
159     }
160 
161     /**
162      * Returns true if the given file contains a line with the string.
163      */
164     private boolean find(Path file, String text) throws IOException {
165         try (Stream<String> stream = Files.lines(file)) {
166             return  stream.anyMatch(line -> line.indexOf(text) >= 0);
167         }
168     }
169 }