Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render method ids #2

Merged
merged 4 commits into from
Apr 9, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/components/ui/index.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ export {
Button,
CheckBox,
Cluster,
DefinitionList,
EmptyTableBody,
FONT_FAMILY,
FaGearIcon,
@@ -14,6 +15,7 @@ export {
Input,
LineClamp,
Loader,
ModelessDialog,
NotificationBar,
PageHeading,
Section,
32 changes: 32 additions & 0 deletions frontend/models/combinedDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
import { Module } from './module'

type BaseDotMetadata = {
id: string
}

type DotSourceMetadata = {
type: 'source'
sourceName: string
} & BaseDotMetadata

type DotDependencyMetadata = {
type: 'dependency'
sourceName: string
methodIds: Array<{
name: string
context: 'class' | 'instance'
human: string
}>
} & BaseDotMetadata

type DotModuleMetadata = {
type: 'module'
moduleName: string
} & BaseDotMetadata

export type DotMetadata = DotSourceMetadata | DotDependencyMetadata | DotModuleMetadata

export type CombinedDefinition = {
ids: number[]
titles: string[]
dot: string
dotMetadata: DotMetadata[]
sources: Array<{ sourceName: string; modules: Module[] }>
}

export type DotSource = {
type: 'source'
sourceName: string
}
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ type Props = {
setGraphOptions: React.Dispatch<React.SetStateAction<GraphOptions>>
}

export const ConfigureViewOptionsDialog: React.FC<Props> = ({ isOpen, onClickClose, graphOptions, setGraphOptions }) => {
export const ConfigureGraphOptionsDialog: React.FC<Props> = ({ isOpen, onClickClose, graphOptions, setGraphOptions }) => {
const [temporaryViewOptions, setTemporaryViewOptions] = useState<GraphOptions>(graphOptions)

const handleDialogClose = () => {
31 changes: 8 additions & 23 deletions frontend/pages/Home/components/DefinitionGraph/DefinitionGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { FC, useCallback, useEffect, useState } from 'react'
import { FC, useCallback, useState } from 'react'
import styled from 'styled-components'

import { Button, FaGearIcon, Heading, LineClamp, Section, Text } from '@/components/ui'
import { color } from '@/constants/theme'
import { CombinedDefinition } from '@/models/combinedDefinition'
import { renderDot } from '@/utils/renderDot'

import { ConfigureViewOptionsDialog, GraphOptions } from './ConfigureGraphOptionsDialog'
import { ConfigureGraphOptionsDialog, GraphOptions } from './ConfigureGraphOptionsDialog'
import { ScrollableSvg } from './ScrollableSvg'

type Props = {
@@ -15,33 +14,19 @@ type Props = {
setGraphOptions: React.Dispatch<React.SetStateAction<GraphOptions>>
}

type DialogType = 'configureViewOptionsDiaglog'
type DialogType = 'configureGraphOptionsDiaglog'

export const DefinitionGraph: FC<Props> = ({ combinedDefinition, graphOptions, setGraphOptions }) => {
const [visibleDialog, setVisibleDialog] = useState<DialogType | null>(null)
const [svg, setSvg] = useState<string>('')

useEffect(() => {
const loadSvg = async () => {
if (combinedDefinition.dot) {
const newSvg = await renderDot(combinedDefinition.dot)
setSvg(newSvg)
} else {
setSvg('')
}
}

loadSvg()
}, [combinedDefinition.dot, setSvg])

const onClickCloseDialog = useCallback(() => {
setVisibleDialog(null)
}, [setVisibleDialog])

return (
<WrapperSection>
<ConfigureViewOptionsDialog
isOpen={visibleDialog === 'configureViewOptionsDiaglog'}
<ConfigureGraphOptionsDialog
isOpen={visibleDialog === 'configureGraphOptionsDiaglog'}
onClickClose={onClickCloseDialog}
graphOptions={graphOptions}
setGraphOptions={setGraphOptions}
@@ -57,14 +42,14 @@ export const DefinitionGraph: FC<Props> = ({ combinedDefinition, graphOptions, s
<Button
size="s"
square
onClick={() => setVisibleDialog('configureViewOptionsDiaglog')}
onClick={() => setVisibleDialog('configureGraphOptionsDiaglog')}
prefix={<FaGearIcon alt="Open Options" />}
>
Open View Options
Open Graph Options
</Button>
</FixedHeightHeading>
<FlexHeightSvgWrapper>
<ScrollableSvg svg={svg} />
<ScrollableSvg combinedDefinition={combinedDefinition} />
</FlexHeightSvgWrapper>
</WrapperSection>
)
77 changes: 77 additions & 0 deletions frontend/pages/Home/components/DefinitionGraph/MetadataDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ComponentProps, FC } from 'react'
import styled from 'styled-components'

import { Link } from '@/components/Link'
import { Base, DefinitionList, Heading, ModelessDialog, Stack } from '@/components/ui'
import { path } from '@/constants/path'
import { spacing } from '@/constants/theme'
import { DotMetadata } from '@/models/combinedDefinition'

type Props = {
dotMetadata: DotMetadata | null
isOpen: boolean
onClose: () => void
top: number
left: number
}

export const MetadataDialog: FC<Props> = ({ dotMetadata, isOpen, onClose, top, left }) => {
const items: ComponentProps<typeof DefinitionList>['items'] = []

switch (dotMetadata?.type) {
case 'source': {
items.push({
term: 'Source Name',
description: <Link to={path.sources.show(dotMetadata.sourceName)}>{dotMetadata.sourceName}</Link>,
})
break
}
case 'dependency': {
items.push({
term: 'Dependency Name',
description: <Link to={path.sources.show(dotMetadata.sourceName)}>{dotMetadata.sourceName}</Link>,
})
items.push({
term: 'Method ID',
description: dotMetadata.methodIds.map((methodId) => (
<p key={`${methodId.context}-${methodId.name}`}>{methodId.human}</p>
)),
})
break
}
case 'module': {
items.push({
term: 'Module Name',
description: <Link to={path.modules.show(dotMetadata.moduleName)}>{dotMetadata.moduleName}</Link>,
})
break
}
}

return (
<ModelessDialog
isOpen={!!(isOpen && dotMetadata)}
header={<ModelessHeading>Description</ModelessHeading>}
onClickClose={onClose}
onPressEscape={onClose}
top={top}
left={left}
>
<Wrapper>
<Stack gap={0.5} as="section">
<DefinitionList items={items} />
</Stack>
</Wrapper>
</ModelessDialog>
)
}

const ModelessHeading = styled(Heading)`
font-size: 1em;
margin: 0;
font-weight: normal;
`

const Wrapper = styled.div`
padding: ${spacing.XS};
`
138 changes: 125 additions & 13 deletions frontend/pages/Home/components/DefinitionGraph/ScrollableSvg.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,141 @@
import React, { FC, useCallback, useState } from 'react'
import { ReactSVGPanZoom, TOOL_PAN } from 'react-svg-pan-zoom'
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
import { ReactSVGPanZoom, TOOL_NONE, TOOL_PAN } from 'react-svg-pan-zoom'
import { ReactSvgPanZoomLoader } from 'react-svg-pan-zoom-loader'
import styled from 'styled-components'

import { useRefSize } from '@/hooks/useRefSize'
import { CombinedDefinition, DotMetadata } from '@/models/combinedDefinition'
import { renderDot } from '@/utils/renderDot'
import { extractSvgSize, getClosestAndSmallestElement, toSVGPoint } from '@/utils/svgHelper'

import { MetadataDialog } from './MetadataDialog'

import type { Tool, Value } from 'react-svg-pan-zoom'

type Props = {
svg: string
combinedDefinition: CombinedDefinition
}

const extractSvgSize = (svg: string) => {
const html: SVGElement = new DOMParser().parseFromString(svg, 'text/html').body.querySelector('svg')!
// Return .cluster, .node, .edge or null
const findClosestElementOnCursor = (event: MouseEvent): SVGGElement | null => {
const svg = (event.target as HTMLElement).closest<SVGSVGElement>('svg')

if (html === null) {
return { width: 0, height: 0 }
// If outside svg, do nothing.
if (!svg) {
return null
}

const width = parseInt(html.getAttribute('width')!.replace(/pt/, ''), 10)!
const height = parseInt(html.getAttribute('height')!.replace(/pt/, ''), 10)!
const elementsUnderCursor = document.elementsFromPoint(event.clientX, event.clientY)
const point = toSVGPoint(svg, event.target! as Element, event.clientX, event.clientY)
const neastElement = getClosestAndSmallestElement(elementsUnderCursor, point)

const neastGeometryElement = neastElement?.closest<SVGGElement>('g.node, g.edge, g.cluster')

return neastGeometryElement ?? null
}

return { width, height }
type ClickedMetadata = {
metadata: DotMetadata
left: number
top: number
}

export const ScrollableSvg: FC<Props> = ({ svg }) => {
export const ScrollableSvg: FC<Props> = ({ combinedDefinition }) => {
const { observeRef, size } = useRefSize<HTMLDivElement>()
const viewerRef = useRef<ReactSVGPanZoom | null>(null)

const [value, setValue] = useState<Value>({} as Value) // NOTE: react-svg-pan-zoom supported blank object as a initial value. but types is not supported.
const [tool, setTool] = useState<Tool>(TOOL_PAN)
const [hoverMetadata, setHoverMetadata] = useState<DotMetadata | null>(null)
const [clickedMetadata, setClickedMetadata] = useState<ClickedMetadata | null>(null)
const [svg, setSvg] = useState<string>('')

const svgSize = extractSvgSize(svg)

const fitToViewerOnMount = useCallback((node: ReactSVGPanZoom) => {
if (node) {
node.fitToViewer('center', 'top')
viewerRef.current = node
} else {
viewerRef.current = null
}
}, [])

// Convert dot to SVG
useEffect(() => {
const loadSvg = async () => {
if (combinedDefinition.dot) {
const newSvg = await renderDot(combinedDefinition.dot)
setSvg(newSvg)
} else {
setSvg('')
}
}

loadSvg()
}, [combinedDefinition.dot, setSvg])

// On click .node, .edge, .cluster
useEffect(() => {
if (tool !== TOOL_NONE) {
setClickedMetadata(null)
return
}

const onClickGeometry = (event: MouseEvent) => {
if (hoverMetadata) {
event.preventDefault()
setClickedMetadata({ metadata: hoverMetadata, left: event.clientX, top: event.clientY })
}
}

document.addEventListener('click', onClickGeometry)

return () => {
document.removeEventListener('click', onClickGeometry)
}
}, [tool, hoverMetadata, setClickedMetadata])

// On hover .node, .edge, .cluster
useEffect(() => {
if (tool !== TOOL_NONE) {
setHoverMetadata(null)
return
}

const onMouseMove = (event: MouseEvent) => {
const element = findClosestElementOnCursor(event)

if (element) {
const metadata = combinedDefinition.dotMetadata.find(({ id }) => element.id === id)
setHoverMetadata(metadata ?? null)
} else {
setHoverMetadata(null)
}
}

document.addEventListener('mousemove', onMouseMove)

return () => {
document.removeEventListener('mousemove', onMouseMove)
}
}, [tool, combinedDefinition.dotMetadata])

const onCloseDialog = useCallback(() => {
setClickedMetadata(null)
}, [setClickedMetadata])

if (!svg) return null

return (
<Wrapper ref={observeRef}>
<Wrapper ref={observeRef} $idOnHover={hoverMetadata?.id}>
<MetadataDialog
dotMetadata={clickedMetadata?.metadata ?? null}
isOpen={!!clickedMetadata}
onClose={onCloseDialog}
top={clickedMetadata?.top ?? 0}
left={clickedMetadata?.left ?? 0}
/>
<ReactSvgPanZoomLoader
svgXML={svg}
render={(content) => (
@@ -67,7 +162,24 @@ export const ScrollableSvg: FC<Props> = ({ svg }) => {
)
}

const Wrapper = styled.div`
const Wrapper = styled.div<{ $idOnHover: string | undefined }>`
height: 100%;
width: 100%;
/* overwride pointer-events: none; for oncursormove events */
.node,
.edge,
.cluster {
pointer-events: all;
}
${(props) =>
props.$idOnHover &&
`
#${props.$idOnHover} {
stroke-width: 3;
}
cursor: pointer;
`}
`
59 changes: 58 additions & 1 deletion frontend/repositories/combinedDefinitionRepository.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
import useSWR from 'swr'

import { path } from '@/constants/path'
import { CombinedDefinition } from '@/models/combinedDefinition'
import { CombinedDefinition, DotMetadata } from '@/models/combinedDefinition'
import { bitIdToIds } from '@/utils/bitId'
import { stringify } from '@/utils/queryString'

import { get } from './httpRequest'

type DotSourceMetadataResponse = {
id: string
type: 'source'
source_name: string
}

type DotDependencyMetadataResponse = {
id: string
type: 'dependency'
source_name: string
method_ids: Array<{
name: string
context: 'class' | 'instance'
}>
}

type DotModuleMetadataResponse = {
id: string
type: 'module'
module_name: string
}

type DotMetadataResponse = DotSourceMetadataResponse | DotDependencyMetadataResponse | DotModuleMetadataResponse

type CombinedDefinitionReponse = {
bit_id: string
titles: string[]
dot: string
dot_metadata: DotMetadataResponse[]
sources: Array<{
source_name: string
modules: Array<{
@@ -19,13 +44,45 @@ type CombinedDefinitionReponse = {
}>
}

const parseDotMetadata = (metadata: DotMetadataResponse): DotMetadata => {
switch (metadata.type) {
case 'source': {
return {
id: metadata.id,
type: metadata.type,
sourceName: metadata.source_name,
}
}
case 'dependency': {
return {
id: metadata.id,
type: metadata.type,
sourceName: metadata.source_name,
methodIds: metadata.method_ids.map((methodId) => ({
name: methodId.name,
context: methodId.context,
human: `${methodId.context === 'class' ? '.' : '#'}${methodId.name}`,
})),
}
}
case 'module': {
return {
id: metadata.id,
type: metadata.type,
moduleName: metadata.module_name,
}
}
}
}

const fetchDefinitionShow = async (requestPath: string): Promise<CombinedDefinition> => {
const response = await get<CombinedDefinitionReponse>(requestPath)

return {
ids: bitIdToIds(BigInt(response.bit_id)),
titles: response.titles,
dot: response.dot,
dotMetadata: response.dot_metadata.map((res) => parseDotMetadata(res)),
sources: response.sources.map((source) => ({
sourceName: source.source_name,
modules: source.modules.map((module) => ({
2 changes: 1 addition & 1 deletion frontend/types/react-svg-pan-zoom.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react'
import { ReactSVGPanZoom as original } from 'react-svg-pan-zoom'

declare module 'react-svg-pan-zoom' {
interface ReactSVGPanZoom {
// @types/react-svg-pan-zoom is not supported alignX and alignY
fitToViewer(alignX: 'left' | 'center' | 'right', alignY: 'top' | 'center' | 'bottom'): void
ViewerDOM: SVGElement | undefined
}
}
73 changes: 73 additions & 0 deletions frontend/utils/svgHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const BLANK_TRANSLATE = { x: 0, y: 0 } as const

// Get the translate value of the element
const getTranslate = (el: Element): { x: number; y: number } => {
const transform = el.getAttribute('transform')
const translate = /translate\(([^, ]+)(?:,|\s+)([^)]+)\)/.exec(transform ?? '')

if (translate) {
return { x: parseFloat(translate[1]), y: parseFloat(translate[2]) }
} else {
return BLANK_TRANSLATE
}
}

// Convert the Point of clientX and clientY to SVG coordinate
export const toSVGPoint = (svg: SVGSVGElement, el: Element, x: number, y: number) => {
let point = svg.createSVGPoint()
point.x = x
point.y = y
const ctm = svg.getScreenCTM()!.inverse()
point = point.matrixTransform(ctm)

let current: Element | null = el
while (current && svg.contains(current)) {
const translate = getTranslate(current)
point.x -= translate.x
point.y -= translate.y

current = current.parentElement?.closest('[transform]') ?? null
}

return point
}

export const extractSvgSize = (svg: string) => {
const html: SVGElement = new DOMParser().parseFromString(svg, 'text/html').body.querySelector('svg')!

if (html === null) {
return { width: 0, height: 0 }
}

const width = parseInt(html.getAttribute('width')!.replace(/pt/, ''), 10)!
const height = parseInt(html.getAttribute('height')!.replace(/pt/, ''), 10)!

return { width, height }
}

const SVGGraphTagNames = ['ellipse', 'path', 'polygon', 'polyline', 'rect', 'circle', 'line']
export const isSVGGeometryElement = (el: Element): el is SVGGeometryElement => SVGGraphTagNames.includes(el.tagName)

export const getClosestAndSmallestElement = (elements: Element[], point: DOMPoint): Element | null => {
let closestElement: Element | null = null
let minDistance = Infinity
let minArea = Infinity

elements.forEach((element) => {
if (isSVGGeometryElement(element)) {
const bbox = element.getBBox()
const centerX = bbox.x + bbox.width / 2
const centerY = bbox.y + bbox.height / 2
const distance = Math.sqrt(Math.pow(centerX - point.x, 2) + Math.pow(centerY - point.y, 2))
const area = bbox.width * bbox.height

if (element.isPointInFill(point) && (area < minArea || (area === minArea && distance < minDistance))) {
closestElement = element
minDistance = distance
minArea = area
}
}
})

return closestElement
}
2 changes: 1 addition & 1 deletion lib/diver_down/definition/method_id.rb
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ def hash
# @param other [DiverDown::Definition::MethodId]
# @return [Integer]
def <=>(other)
[name, context] <=> [other.name, other.context]
[context, name] <=> [other.context, other.name]
end

# @param other [Object, DiverDown::Definition::Source]
5 changes: 4 additions & 1 deletion lib/diver_down/web/action.rb
Original file line number Diff line number Diff line change
@@ -166,10 +166,13 @@ def combine_definitions(bit_id, compound, concentrate)
end

if definition
definition_to_dot = DiverDown::Web::DefinitionToDot.new(definition, compound:, concentrate:)

json(
titles:,
bit_id: DiverDown::Web::BitId.ids_to_bit_id(valid_ids).to_s,
dot: DiverDown::Web::DefinitionToDot.new(definition, compound:, concentrate:).to_s,
dot: definition_to_dot.to_s,
dot_metadata: definition_to_dot.metadata,
sources: definition.sources.map do
{
source_name: _1.source_name,
135 changes: 64 additions & 71 deletions lib/diver_down/web/definition_to_dot.rb
Original file line number Diff line number Diff line change
@@ -1,82 +1,71 @@
# frozen_string_literal: true

require 'json'
require 'cgi'

module DiverDown
class Web
class DefinitionToDot
ATTRIBUTE_DELIMITER = ' '

class SourceDecorator < BasicObject
attr_reader :dependencies
class MetadataStore
attr_reader :to_a

# @param source [DiverDown::Definition::Source]
def initialize(source)
@source = source
@dependencies = source.dependencies.map { DependencyDecorator.new(_1) }
def initialize
@prefix = 'graph_'
@to_a = []
end

# @return [String]
def label
@source.source_name
# @param type [Symbol]
# @param record [DiverDown::Definition::Source, DiverDown::Definition::Dependency, DiverDown::Definition::Modulee]
def issue_id(record)
metadata = case record
when DiverDown::Definition::Source
source_to_metadata(record)
when DiverDown::Definition::Dependency
dependency_to_metadata(record)
when DiverDown::Definition::Modulee
module_to_metadata(record)
else
raise NotImplementedError, "not implemented yet #{record.class}"
end

id = "#{@prefix}#{@to_a.length + 1}"
@to_a.push(metadata.merge(id:))
id
end

# @return [String, nil]
def tooltip
nil
end
private

# @param action [Symbol]
# @param args [Array]
# @param options [Hash, nil]
# @param block [Proc, nil]
def method_missing(action, ...)
if @source.respond_to?(action, true)
@source.send(action, ...)
else
super
end
def length
@to_a.length
end

# @param action [Symbol]
# @param include_private [Boolean]
# @return [Boolean]
def respond_to_missing?(action, include_private = false)
super || @source.respond_to?(action, include_private)
def source_to_metadata(record)
{
type: 'source',
source_name: record.source_name,
}
end
end

class DependencyDecorator < BasicObject
# @param dependency [DiverDown::Definition::dependency]
def initialize(dependency)
@dependency = dependency
def dependency_to_metadata(record)
{
type: 'dependency',
source_name: record.source_name,
method_ids: record.method_ids.sort.map do
{
name: _1.name,
context: _1.context,
}
end,
}
end

# @return [String]
def label
@dependency.dependency
end

# @return [String, nil]
def tooltip
nil
end

# @param action [Symbol]
# @param args [Array]
# @param options [Hash, nil]
# @param block [Proc, nil]
def method_missing(action, ...)
if @dependency.respond_to?(action, true)
@dependency.send(action, ...)
else
super
end
end

# @param action [Symbol]
# @param include_private [Boolean]
# @return [Boolean]
def respond_to_missing?(action, include_private = false)
super || @dependency.respond_to?(action, include_private)
def module_to_metadata(record)
{
type: 'module',
module_name: record.module_name,
}
end
end

@@ -90,13 +79,17 @@ def initialize(definition, compound: false, concentrate: false)
@compound = compound
@compound_map = Hash.new { |h, k| h[k] = Set.new } # { ltail => Set<lhead> }
@concentrate = concentrate
@metadata_store = MetadataStore.new
end

# @return [Array<Hash>]
def metadata
@metadata_store.to_a
end

# @return [String]
def to_s
sources = definition.sources
.sort_by(&:source_name)
.map { SourceDecorator.new(_1) }
sources = definition.sources.sort_by(&:source_name)

io.puts %(strict digraph "#{definition.title}" {)
io.indented do
@@ -116,13 +109,15 @@ def to_s

def insert_source(source)
if source.modules.empty?
io.puts %("#{source.source_name}" #{build_attributes(label: source.label)})
io.puts %("#{source.source_name}" #{build_attributes(label: source.source_name, id: @metadata_store.issue_id(source))})
else
insert_modules(source)
end

source.dependencies.each do
attributes = {}
attributes = {
id: @metadata_store.issue_id(_1),
}
ltail = module_label(*source.modules)
lhead = module_label(*definition.source(_1.source_name).modules)
skip_rendering = false
@@ -156,12 +151,10 @@ def insert_modules(source)
last_module_writer = proc do
io.puts %(#{' ' unless modules.empty?}subgraph "#{module_label(last_module)}" {), indent: false
io.indented do
source_attributes = build_attributes(label: last_module.module_name, _wrap: false)
module_attributes = build_attributes(label: source.source_name)
module_attributes = build_attributes(label: last_module.module_name, id: @metadata_store.issue_id(last_module), _wrap: false)
source_attributes = build_attributes(label: source.source_name, id: @metadata_store.issue_id(source))

io.write %(#{source_attributes} "#{source.source_name}")
io.write(" #{module_attributes}", indent: false) if module_attributes
io.write "\n"
io.puts %(#{module_attributes} "#{source.source_name}" #{source_attributes})
end
io.puts '}'
end
@@ -171,7 +164,7 @@ def insert_modules(source)
proc do
io.puts %(subgraph "#{module_label(mod)}" {)
io.indented do
attributes = build_attributes(label: mod.module_name, _wrap: false)
attributes = build_attributes(label: mod.module_name, id: @metadata_store.issue_id(mod), _wrap: false)
io.write attributes
next_writer.call
end
4 changes: 2 additions & 2 deletions spec/diver_down/definition/method_id_spec.rb
Original file line number Diff line number Diff line change
@@ -62,8 +62,8 @@
),
].shuffle

expect(array.sort.map(&:name)).to eq(%w[a b b c])
expect(array.sort.map(&:context)).to eq(%w[class class instance class])
expect(array.sort.map(&:name)).to eq(%w[a b c b])
expect(array.sort.map(&:context)).to eq(%w[class class class instance])
end
end

32 changes: 16 additions & 16 deletions spec/diver_down/trace/tracer_spec.rb
Original file line number Diff line number Diff line change
@@ -378,15 +378,15 @@ def fill_default(hash)
source_name: 'AntipollutionModule::B',
method_ids: [
{
name: 'call_c',
context: 'instance',
name: 'new',
context: 'class',
paths: [
match(/tracer_instance\.rb:\d+/),
],
},
{
name: 'new',
context: 'class',
name: 'call_c',
context: 'instance',
paths: [
match(/tracer_instance\.rb:\d+/),
],
@@ -402,15 +402,15 @@ def fill_default(hash)
source_name: 'AntipollutionModule::C',
method_ids: [
{
name: 'call_d',
context: 'instance',
name: 'new',
context: 'class',
paths: [
match(/tracer_instance\.rb:\d+/),
],
},
{
name: 'new',
context: 'class',
name: 'call_d',
context: 'instance',
paths: [
match(/tracer_instance\.rb:\d+/),
],
@@ -462,15 +462,15 @@ def fill_default(hash)
source_name: 'AntipollutionModule::B',
method_ids: [
{
name: 'call_c',
context: 'instance',
name: 'new',
context: 'class',
paths: [
match(/tracer_subclass\.rb:\d+/),
],
},
{
name: 'new',
context: 'class',
name: 'call_c',
context: 'instance',
paths: [
match(/tracer_subclass\.rb:\d+/),
],
@@ -486,15 +486,15 @@ def fill_default(hash)
source_name: 'AntipollutionModule::C',
method_ids: [
{
name: 'call_d',
context: 'instance',
name: 'new',
context: 'class',
paths: [
match(/tracer_subclass\.rb:\d+/),
],
},
{
name: 'new',
context: 'class',
name: 'call_d',
context: 'instance',
paths: [
match(/tracer_subclass\.rb:\d+/),
],
135 changes: 119 additions & 16 deletions spec/diver_down/web/definition_to_dot_spec.rb
Original file line number Diff line number Diff line change
@@ -34,11 +34,22 @@ def build_definition(title: 'title', sources: [])
]
)

expect(described_class.new(definition).to_s).to eq(<<~DOT)
instance = described_class.new(definition)
expect(instance.to_s).to eq(<<~DOT)
strict digraph "title" {
"a.rb" [label="a.rb"]
"a.rb" [label="a.rb" id="graph_1"]
}
DOT

expect(instance.metadata).to eq(
[
{
id: 'graph_1',
type: 'source',
source_name: 'a.rb',
},
]
)
end
end

@@ -60,13 +71,33 @@ def build_definition(title: 'title', sources: [])
]
)

expect(described_class.new(definition).to_s).to eq(<<~DOT)
instance = described_class.new(definition)
expect(instance.to_s).to eq(<<~DOT)
strict digraph "title" {
"a.rb" [label="a.rb"]
"a.rb" -> "b.rb"
"b.rb" [label="b.rb"]
"a.rb" [label="a.rb" id="graph_1"]
"a.rb" -> "b.rb" [id="graph_2"]
"b.rb" [label="b.rb" id="graph_3"]
}
DOT

expect(instance.metadata).to eq(
[
{
id: 'graph_1',
type: 'source',
source_name: 'a.rb',
}, {
id: 'graph_2',
type: 'dependency',
source_name: 'b.rb',
method_ids: [],
}, {
id: 'graph_3',
type: 'source',
source_name: 'b.rb',
},
]
)
end
end

@@ -87,15 +118,35 @@ def build_definition(title: 'title', sources: [])
]
)

expect(described_class.new(definition).to_s).to eq(<<~DOT)
instance = described_class.new(definition)

expect(instance.to_s).to eq(<<~DOT)
strict digraph "title" {
subgraph "cluster_A" {
label="A" subgraph "cluster_B" {
label="B" "a.rb" [label="a.rb"]
label="A" id="graph_1" subgraph "cluster_B" {
label="B" id="graph_2" "a.rb" [label="a.rb" id="graph_3"]
}
}
}
DOT

expect(instance.metadata).to eq(
[
{
id: 'graph_1',
type: 'module',
module_name: 'A',
}, {
id: 'graph_2',
type: 'module',
module_name: 'B',
}, {
id: 'graph_3',
type: 'source',
source_name: 'a.rb',
},
]
)
end

it 'returns compound digraph if compound = true' do
@@ -135,21 +186,62 @@ def build_definition(title: 'title', sources: [])
]
)

expect(described_class.new(definition, compound: true).to_s).to eq(<<~DOT)
instance = described_class.new(definition, compound: true)
expect(instance.to_s).to eq(<<~DOT)
strict digraph "title" {
compound=true
subgraph "cluster_A" {
label="A" "a.rb" [label="a.rb"]
label="A" id="graph_1" "a.rb" [label="a.rb" id="graph_2"]
}
"a.rb" -> "b.rb" [ltail="cluster_A" lhead="cluster_B" minlen="3"]
"a.rb" -> "b.rb" [id="graph_3" ltail="cluster_A" lhead="cluster_B" minlen="3"]
subgraph "cluster_B" {
label="B" "b.rb" [label="b.rb"]
label="B" id="graph_5" "b.rb" [label="b.rb" id="graph_6"]
}
subgraph "cluster_B" {
label="B" "c.rb" [label="c.rb"]
label="B" id="graph_7" "c.rb" [label="c.rb" id="graph_8"]
}
}
DOT

expect(instance.metadata).to eq(
[
{
id: 'graph_1',
type: 'module',
module_name: 'A',
}, {
id: 'graph_2',
type: 'source',
source_name: 'a.rb',
}, {
id: 'graph_3',
type: 'dependency',
source_name: 'b.rb',
method_ids: [],
}, {
id: 'graph_4',
type: 'dependency',
source_name: 'c.rb',
method_ids: [],
}, {
id: 'graph_5',
type: 'module',
module_name: 'B',
}, {
id: 'graph_6',
type: 'source',
source_name: 'b.rb',
}, {
id: 'graph_7',
type: 'module',
module_name: 'B',
}, {
id: 'graph_8',
type: 'source',
source_name: 'c.rb',
},
]
)
end

it 'returns concentrate digraph if concentrate = true' do
@@ -161,12 +253,23 @@ def build_definition(title: 'title', sources: [])
]
)

expect(described_class.new(definition, concentrate: true).to_s).to eq(<<~DOT)
instance = described_class.new(definition, concentrate: true)
expect(instance.to_s).to eq(<<~DOT)
strict digraph "title" {
concentrate=true
"a.rb" [label="a.rb"]
"a.rb" [label="a.rb" id="graph_1"]
}
DOT

expect(instance.metadata).to eq(
[
{
id: 'graph_1',
type: 'source',
source_name: 'a.rb',
},
]
)
end
end
end