From c536b5f0f5c3d5fdbf84248009da21fa3fd18d8d Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Tue, 4 Sep 2018 23:46:10 +0200 Subject: [PATCH] feat: add process.takeHeapSnapshot() / webContents.takeHeapSnapshot() --- atom/browser/api/atom_api_web_contents.cc | 24 ++++++++ atom/browser/api/atom_api_web_contents.h | 3 + atom/common/api/api_messages.h | 5 ++ atom/common/api/atom_bindings.cc | 56 +++++++++++++++++++ atom/common/api/atom_bindings.h | 3 + .../common/api/heap_snapshot_output_stream.cc | 52 +++++++++++++++++ atom/common/api/heap_snapshot_output_stream.h | 34 +++++++++++ atom/common/api/remote_object_freer.cc | 16 +----- atom/renderer/api/atom_api_renderer_ipc.cc | 4 +- atom/renderer/api/atom_api_renderer_ipc.h | 6 ++ .../atom_sandboxed_renderer_client.cc | 4 +- docs/api/process.md | 10 ++++ docs/api/web-contents.md | 8 +++ filenames.gni | 3 + lib/browser/api/web-contents.js | 14 +++++ lib/renderer/heap-snapshot.js | 16 ++++++ lib/renderer/init.js | 1 + lib/sandboxed_renderer/init.js | 2 + spec/api-process-spec.js | 9 +++ spec/api-web-contents-spec.js | 45 +++++++++++++++ 20 files changed, 297 insertions(+), 18 deletions(-) create mode 100644 atom/common/api/heap_snapshot_output_stream.cc create mode 100644 atom/common/api/heap_snapshot_output_stream.h create mode 100644 lib/renderer/heap-snapshot.js diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 4fadf30545a69..e8a444329fbf9 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -20,6 +20,8 @@ #include "atom/browser/lib/bluetooth_chooser.h" #include "atom/browser/native_window.h" #include "atom/browser/net/atom_network_delegate.h" +#include "atom/common/api/heap_snapshot_output_stream.h" +#include "base/threading/thread_restrictions.h" #if defined(ENABLE_OSR) #include "atom/browser/osr/osr_output_device.h" #include "atom/browser/osr/osr_render_widget_host_view.h" @@ -299,6 +301,10 @@ struct WebContents::FrameDispatchHelper { IPC::Message* message) { api_web_contents->OnRendererMessageSync(rfh, channel, args, message); } + + void OnCreateHeapSnapshotFile(IPC::Message* message) { + api_web_contents->OnCreateHeapSnapshotFile(rfh, message); + } }; WebContents::WebContents(v8::Isolate* isolate, @@ -1036,6 +1042,9 @@ bool WebContents::OnMessageReceived(const IPC::Message& message, IPC_MESSAGE_HANDLER(AtomAutofillFrameHostMsg_ShowPopup, ShowAutofillPopup) IPC_MESSAGE_HANDLER(AtomAutofillFrameHostMsg_HidePopup, HideAutofillPopup) #endif + IPC_MESSAGE_FORWARD_DELAY_REPLY( + AtomFrameHostMsg_CreateHeapSnapshotFile, &helper, + FrameDispatchHelper::OnCreateHeapSnapshotFile) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() @@ -2108,6 +2117,21 @@ void WebContents::OnRendererMessageTo(content::RenderFrameHost* frame_host, } } +void WebContents::OnCreateHeapSnapshotFile(content::RenderFrameHost* frame_host, + IPC::Message* message) { + base::ThreadRestrictions::ScopedAllowIO allow_io; + + auto file_path = HeapSnapshotOutputStream::GetFilePath(); + + base::File file(file_path, + base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); + + AtomFrameHostMsg_CreateHeapSnapshotFile::WriteReplyParams( + message, file_path, IPC::TakePlatformFileForTransit(std::move(file))); + + frame_host->Send(message); +} + // static mate::Handle WebContents::CreateFrom( v8::Isolate* isolate, diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 558a1a27adf18..588ceaa0caca2 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -434,6 +434,9 @@ class WebContents : public mate::TrackableObject, const std::string& channel, const base::ListValue& args); + void OnCreateHeapSnapshotFile(content::RenderFrameHost* frame_host, + IPC::Message* message); + // Called when received a synchronous message from renderer to // set temporary zoom level. void OnSetTemporaryZoomLevel(content::RenderFrameHost* frame_host, diff --git a/atom/common/api/api_messages.h b/atom/common/api/api_messages.h index e7317736e0766..afc3aa94fe64d 100644 --- a/atom/common/api/api_messages.h +++ b/atom/common/api/api_messages.h @@ -10,6 +10,7 @@ #include "content/public/common/common_param_traits.h" #include "content/public/common/referrer.h" #include "ipc/ipc_message_macros.h" +#include "ipc/ipc_platform_file.h" #include "ui/gfx/geometry/rect_f.h" #include "ui/gfx/ipc/gfx_param_traits.h" #include "url/gurl.h" @@ -76,3 +77,7 @@ IPC_SYNC_MESSAGE_ROUTED0_1(AtomFrameHostMsg_GetZoomLevel, double /* result */) IPC_MESSAGE_ROUTED2(AtomFrameHostMsg_PDFSaveURLAs, GURL /* url */, content::Referrer /* referrer */) + +IPC_SYNC_MESSAGE_ROUTED0_2(AtomFrameHostMsg_CreateHeapSnapshotFile, + base::FilePath /* file_path */, + IPC::PlatformFileForTransit /* file_handle */) diff --git a/atom/common/api/atom_bindings.cc b/atom/common/api/atom_bindings.cc index 32f05baabbb90..038fe2add1058 100644 --- a/atom/common/api/atom_bindings.cc +++ b/atom/common/api/atom_bindings.cc @@ -8,16 +8,22 @@ #include #include +#include "atom/common/api/api_messages.h" +#include "atom/common/api/heap_snapshot_output_stream.h" #include "atom/common/api/locker.h" #include "atom/common/atom_version.h" #include "atom/common/chrome_version.h" +#include "atom/common/native_mate_converters/file_path_converter.h" #include "atom/common/native_mate_converters/string16_converter.h" #include "atom/common/node_includes.h" +#include "atom/renderer/api/atom_api_renderer_ipc.h" #include "base/logging.h" #include "base/process/process_info.h" #include "base/process/process_metrics_iocounters.h" #include "base/sys_info.h" +#include "content/public/renderer/render_frame.h" #include "native_mate/dictionary.h" +#include "v8/include/v8-profiler.h" namespace atom { @@ -60,6 +66,7 @@ void AtomBindings::BindTo(v8::Isolate* isolate, v8::Local process) { dict.SetMethod("getCPUUsage", base::Bind(&AtomBindings::GetCPUUsage, base::Unretained(metrics_.get()))); dict.SetMethod("getIOCounters", &GetIOCounters); + dict.SetMethod("takeHeapSnapshot", &TakeHeapSnapshot); #if defined(OS_POSIX) dict.SetMethod("setFdLimit", &base::SetFdLimit); #endif @@ -238,4 +245,53 @@ v8::Local AtomBindings::GetIOCounters(v8::Isolate* isolate) { return dict.GetHandle(); } +// static +base::FilePath AtomBindings::TakeHeapSnapshot(v8::Isolate* isolate, + mate::Arguments* args) { + base::ThreadRestrictions::ScopedAllowIO allow_io; + + auto file_path = HeapSnapshotOutputStream::GetFilePath(); + + base::File file(file_path, + base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); + + if (!file.IsValid()) { + auto* render_frame = GetCurrentRenderFrame(); + if (!render_frame) { + args->ThrowError("takeHeapSnapshot failed"); + return base::FilePath(); + } + + IPC::PlatformFileForTransit file_handle; + auto* message = new AtomFrameHostMsg_CreateHeapSnapshotFile( + render_frame->GetRoutingID(), &file_path, &file_handle); + + if (!render_frame->Send(message)) { + args->ThrowError("takeHeapSnapshot failed"); + return base::FilePath(); + } + + file = IPC::PlatformFileForTransitToFile(file_handle); + } + + if (!file.IsValid()) { + args->ThrowError("takeHeapSnapshot failed - cannot create file"); + return base::FilePath(); + } + + auto* snap = isolate->GetHeapProfiler()->TakeHeapSnapshot(); + + HeapSnapshotOutputStream stream(&file); + snap->Serialize(&stream, v8::HeapSnapshot::kJSON); + + const_cast(snap)->Delete(); + + if (!stream.IsComplete()) { + args->ThrowError("takeHeapSnapshot failed - snapshot incomplete"); + return base::FilePath(); + } + + return file_path; +} + } // namespace atom diff --git a/atom/common/api/atom_bindings.h b/atom/common/api/atom_bindings.h index 9ae736fe160b6..1d1e720fd2dd4 100644 --- a/atom/common/api/atom_bindings.h +++ b/atom/common/api/atom_bindings.h @@ -8,6 +8,7 @@ #include #include +#include "base/files/file_path.h" #include "base/macros.h" #include "base/process/process_metrics.h" #include "base/strings/string16.h" @@ -43,6 +44,8 @@ class AtomBindings { static v8::Local GetCPUUsage(base::ProcessMetrics* metrics, v8::Isolate* isolate); static v8::Local GetIOCounters(v8::Isolate* isolate); + static base::FilePath TakeHeapSnapshot(v8::Isolate* isolate, + mate::Arguments* args); private: void ActivateUVLoop(v8::Isolate* isolate); diff --git a/atom/common/api/heap_snapshot_output_stream.cc b/atom/common/api/heap_snapshot_output_stream.cc new file mode 100644 index 0000000000000..a2c3f12dd9f92 --- /dev/null +++ b/atom/common/api/heap_snapshot_output_stream.cc @@ -0,0 +1,52 @@ +// Copyright (c) 2018 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/common/api/heap_snapshot_output_stream.h" + +#include "base/path_service.h" +#include "base/strings/stringprintf.h" +#include "brightray/browser/brightray_paths.h" +#include "uv.h" // NOLINT(build/include) + +namespace atom { + +HeapSnapshotOutputStream::HeapSnapshotOutputStream(base::File* file) + : file_(file) { + DCHECK(file_); +} + +bool HeapSnapshotOutputStream::IsComplete() const { + return is_complete_; +} + +int HeapSnapshotOutputStream::GetChunkSize() { + return 65536; +} + +void HeapSnapshotOutputStream::EndOfStream() { + is_complete_ = true; +} + +v8::OutputStream::WriteResult HeapSnapshotOutputStream::WriteAsciiChunk( + char* data, + int size) { + auto bytes_written = file_->WriteAtCurrentPos(data, size); + return bytes_written == size ? kContinue : kAbort; +} + +// static +base::FilePath HeapSnapshotOutputStream::GetFilePath() { + base::FilePath user_data; + if (!PathService::Get(brightray::DIR_USER_DATA, &user_data)) + return base::FilePath(); + + auto now = uv_hrtime(); + auto sec = static_cast(now / 1000000); + auto usec = static_cast(now % 1000000); + auto filename = base::StringPrintf("heapdump-%u-%u.heapsnapshot", sec, usec); + + return user_data.AppendASCII(filename.c_str()); +} + +} // namespace atom diff --git a/atom/common/api/heap_snapshot_output_stream.h b/atom/common/api/heap_snapshot_output_stream.h new file mode 100644 index 0000000000000..d19fbd5e867dd --- /dev/null +++ b/atom/common/api/heap_snapshot_output_stream.h @@ -0,0 +1,34 @@ +// Copyright (c) 2018 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_COMMON_API_HEAP_SNAPSHOT_OUTPUT_STREAM_H_ +#define ATOM_COMMON_API_HEAP_SNAPSHOT_OUTPUT_STREAM_H_ + +#include "base/files/file.h" +#include "base/files/file_path.h" +#include "v8/include/v8-profiler.h" + +namespace atom { + +class HeapSnapshotOutputStream : public v8::OutputStream { + public: + explicit HeapSnapshotOutputStream(base::File* file); + + bool IsComplete() const; + + // v8::OutputStream + int GetChunkSize() override; + void EndOfStream() override; + v8::OutputStream::WriteResult WriteAsciiChunk(char* data, int size) override; + + static base::FilePath GetFilePath(); + + private: + base::File* file_ = nullptr; + bool is_complete_ = false; +}; + +} // namespace atom + +#endif // ATOM_COMMON_API_HEAP_SNAPSHOT_OUTPUT_STREAM_H_ diff --git a/atom/common/api/remote_object_freer.cc b/atom/common/api/remote_object_freer.cc index 7429a93bad288..548da46e088fb 100644 --- a/atom/common/api/remote_object_freer.cc +++ b/atom/common/api/remote_object_freer.cc @@ -5,27 +5,13 @@ #include "atom/common/api/remote_object_freer.h" #include "atom/common/api/api_messages.h" +#include "atom/renderer/api/atom_api_renderer_ipc.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "content/public/renderer/render_frame.h" -#include "third_party/blink/public/web/web_local_frame.h" - -using blink::WebLocalFrame; namespace atom { -namespace { - -content::RenderFrame* GetCurrentRenderFrame() { - WebLocalFrame* frame = WebLocalFrame::FrameForCurrentContext(); - if (!frame) - return nullptr; - - return content::RenderFrame::FromWebFrame(frame); -} - -} // namespace - // static void RemoteObjectFreer::BindTo(v8::Isolate* isolate, v8::Local target, diff --git a/atom/renderer/api/atom_api_renderer_ipc.cc b/atom/renderer/api/atom_api_renderer_ipc.cc index 5f1947be20204..772f62f8701d1 100644 --- a/atom/renderer/api/atom_api_renderer_ipc.cc +++ b/atom/renderer/api/atom_api_renderer_ipc.cc @@ -17,8 +17,6 @@ using content::RenderFrame; namespace atom { -namespace api { - RenderFrame* GetCurrentRenderFrame() { WebLocalFrame* frame = WebLocalFrame::FrameForCurrentContext(); if (!frame) @@ -27,6 +25,8 @@ RenderFrame* GetCurrentRenderFrame() { return RenderFrame::FromWebFrame(frame); } +namespace api { + void Send(mate::Arguments* args, const std::string& channel, const base::ListValue& arguments) { diff --git a/atom/renderer/api/atom_api_renderer_ipc.h b/atom/renderer/api/atom_api_renderer_ipc.h index b517e78301e87..81c52dda67ca5 100644 --- a/atom/renderer/api/atom_api_renderer_ipc.h +++ b/atom/renderer/api/atom_api_renderer_ipc.h @@ -10,8 +10,14 @@ #include "base/values.h" #include "native_mate/arguments.h" +namespace content { +class RenderFrame; +} + namespace atom { +content::RenderFrame* GetCurrentRenderFrame(); + namespace api { void Send(mate::Arguments* args, diff --git a/atom/renderer/atom_sandboxed_renderer_client.cc b/atom/renderer/atom_sandboxed_renderer_client.cc index d312d4c9cac26..dbabb0fb33df5 100644 --- a/atom/renderer/atom_sandboxed_renderer_client.cc +++ b/atom/renderer/atom_sandboxed_renderer_client.cc @@ -6,6 +6,7 @@ #include "atom/common/api/api_messages.h" #include "atom/common/api/atom_bindings.h" +#include "atom/common/native_mate_converters/file_path_converter.h" #include "atom/common/native_mate_converters/string16_converter.h" #include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/node_bindings.h" @@ -20,8 +21,8 @@ #include "chrome/renderer/printing/print_web_view_helper.h" #include "content/public/renderer/render_frame.h" #include "native_mate/dictionary.h" -#include "third_party/blink/public/web/web_document.h" #include "third_party/blink/public/web/blink.h" +#include "third_party/blink/public/web/web_document.h" #include "atom/common/node_includes.h" #include "atom_natives.h" // NOLINT: This file is generated with js2c @@ -157,6 +158,7 @@ void AtomSandboxedRendererClient::InitializeBindings( b.SetMethod("getCPUUsage", base::Bind(&AtomBindings::GetCPUUsage, base::Unretained(metrics_.get()))); b.SetMethod("getIOCounters", &AtomBindings::GetIOCounters); + b.SetMethod("takeHeapSnapshot", &AtomBindings::TakeHeapSnapshot); // Pass in CLI flags needed to setup the renderer base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); diff --git a/docs/api/process.md b/docs/api/process.md index 691238a737a42..958a5f02da827 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -171,6 +171,16 @@ Returns `Object`: Returns an object giving memory usage statistics about the entire system. Note that all statistics are reported in Kilobytes. + + ### `process.hang()` Causes the main thread of the current process hang. diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index f9bc611317743..01b34b4fb3359 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1459,6 +1459,14 @@ Returns `Integer` - The Chromium internal `pid` of the associated renderer. Can be compared to the `frameProcessId` passed by frame specific navigation events (e.g. `did-frame-navigate`) +#### `contents.takeHeapSnapshot()` + +Returns `Promise` - The file path of the saved V8 heap snapshot. + +Takes a V8 heap snapshot and saves it to the userData directory. +The promise resolves with the file path of the saved file. +The promise will be rejected in case of failure. + ### Instance Properties #### `contents.id` diff --git a/filenames.gni b/filenames.gni index 29fc65b928959..569987e8a7148 100644 --- a/filenames.gni +++ b/filenames.gni @@ -63,6 +63,7 @@ filenames = { "lib/renderer/callbacks-registry.js", "lib/renderer/chrome-api.js", "lib/renderer/content-scripts-injector.js", + "lib/renderer/heap-snapshot.js", "lib/renderer/init.js", "lib/renderer/inspector.js", "lib/renderer/override.js", @@ -445,6 +446,8 @@ filenames = { "atom/common/api/event_emitter_caller.cc", "atom/common/api/event_emitter_caller.h", "atom/common/api/features.cc", + "atom/common/api/heap_snapshot_output_stream.cc", + "atom/common/api/heap_snapshot_output_stream.h", "atom/common/api/locker.cc", "atom/common/api/locker.h", "atom/common/api/object_life_monitor.cc", diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index f7a3394a57ef3..9e753c7921a34 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -160,6 +160,20 @@ WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callba } } +WebContents.prototype.takeHeapSnapshot = function (...args) { + return new Promise((resolve, reject) => { + const requestId = getNextId() + ipcMain.once(`ELECTRON_BROWSER_TAKE_HEAP_SNAPSHOT_RESULT_${requestId}`, (event, error, result) => { + if (error) { + reject(errorUtils.deserialize(error)) + } else { + resolve(result) + } + }) + this.send('ELECTRON_RENDERER_TAKE_HEAP_SNAPSHOT', requestId, args) + }) +} + // Translate the options of printToPDF. WebContents.prototype.printToPDF = function (options, callback) { const printingSetting = Object.assign({}, defaultPrintingSetting) diff --git a/lib/renderer/heap-snapshot.js b/lib/renderer/heap-snapshot.js new file mode 100644 index 0000000000000..aa0a1e9eb8a2d --- /dev/null +++ b/lib/renderer/heap-snapshot.js @@ -0,0 +1,16 @@ +const { ipcRenderer } = require('electron') +const errorUtils = require('../common/error-utils') + +module.exports = (process) => { + ipcRenderer.on('ELECTRON_RENDERER_TAKE_HEAP_SNAPSHOT', (event, requestId, args) => { + new Promise(resolve => { + resolve(process.takeHeapSnapshot(...args)) + }).then(result => { + return [null, result] + }, error => { + return [errorUtils.serialize(error)] + }).then(responseArgs => { + event.sender.send(`ELECTRON_BROWSER_TAKE_HEAP_SNAPSHOT_RESULT_${requestId}`, ...responseArgs) + }) + }) +} diff --git a/lib/renderer/init.js b/lib/renderer/init.js index e8abb9c5ba93f..11e2154a5eb67 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -40,6 +40,7 @@ const { } = require('./security-warnings') require('./web-frame-init')() +require('./heap-snapshot')(process) // Process command line arguments. let nodeIntegration = 'false' diff --git a/lib/sandboxed_renderer/init.js b/lib/sandboxed_renderer/init.js index 6a79d07a34ec5..475727e919e3a 100644 --- a/lib/sandboxed_renderer/init.js +++ b/lib/sandboxed_renderer/init.js @@ -42,6 +42,7 @@ const { } = electron.ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD') require('../renderer/web-frame-init')() +require('../renderer/heap-snapshot')(binding) // Pass different process object to the preload script(which should not have // access to things like `process.atomBinding`). @@ -52,6 +53,7 @@ preloadProcess.getHeapStatistics = () => binding.getHeapStatistics() preloadProcess.getSystemMemoryInfo = () => binding.getSystemMemoryInfo() preloadProcess.getCPUUsage = () => binding.getCPUUsage() preloadProcess.getIOCounters = () => binding.getIOCounters() +preloadProcess.takeHeapSnapshot = () => binding.takeHeapSnapshot() preloadProcess.argv = process.argv = binding.getArgv() preloadProcess.execPath = process.execPath = binding.getExecPath() preloadProcess.pid = process.pid = binding.getPid() diff --git a/spec/api-process-spec.js b/spec/api-process-spec.js index 65f915ba5c543..1cbca2142ae78 100644 --- a/spec/api-process-spec.js +++ b/spec/api-process-spec.js @@ -1,4 +1,5 @@ const { expect } = require('chai') +const fs = require('fs') describe('process module', () => { describe('process.getCreationTime()', () => { @@ -67,4 +68,12 @@ describe('process module', () => { expect(heapStats.doesZapGarbage).to.be.a('boolean') }) }) + + describe('process.takeHeapSnapshot()', () => { + it('returns a file path', () => { + const filePath = process.takeHeapSnapshot() + expect(filePath).to.be.a('string') + fs.unlink(filePath, () => {}) + }) + }) }) diff --git a/spec/api-web-contents-spec.js b/spec/api-web-contents-spec.js index 2342bfaaa1423..0462e3dcf05ed 100644 --- a/spec/api-web-contents-spec.js +++ b/spec/api-web-contents-spec.js @@ -1,6 +1,7 @@ 'use strict' const assert = require('assert') +const fs = require('fs') const http = require('http') const path = require('path') const { closeWindow } = require('./window-helpers') @@ -799,4 +800,48 @@ describe('webContents module', () => { w.loadURL('about:blank') }) }) + + describe('takeHeapSnapshot()', () => { + let filePath + + beforeEach(() => { + filePath = null + }) + + afterEach(done => { + if (filePath) { + fs.unlink(filePath, () => done()) + } + filePath = null + }) + + it('works with non-sandboxed renderers', async () => { + w.destroy() + w = new BrowserWindow({ + show: false + }) + + w.loadURL('about:blank') + await emittedOnce(w.webContents, 'did-finish-load') + + filePath = await w.webContents.takeHeapSnapshot() + expect(filePath).to.be.a('string') + }) + + it('works with sandboxed renderers', async () => { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }) + + w.loadURL('about:blank') + await emittedOnce(w.webContents, 'did-finish-load') + + filePath = await w.webContents.takeHeapSnapshot() + expect(filePath).to.be.a('string') + }) + }) })