...

Source file src/oss.terrastruct.com/d2/d2renderers/d2sketch/sketch_test.go

Documentation: oss.terrastruct.com/d2/d2renderers/d2sketch

     1  package d2sketch_test
     2  
     3  import (
     4  	"context"
     5  	"encoding/xml"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"cdr.dev/slog"
    12  
    13  	tassert "github.com/stretchr/testify/assert"
    14  
    15  	"oss.terrastruct.com/util-go/assert"
    16  	"oss.terrastruct.com/util-go/diff"
    17  	"oss.terrastruct.com/util-go/go2"
    18  
    19  	"oss.terrastruct.com/d2/d2graph"
    20  	"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
    21  	"oss.terrastruct.com/d2/d2layouts/d2elklayout"
    22  	"oss.terrastruct.com/d2/d2lib"
    23  	"oss.terrastruct.com/d2/d2renderers/d2fonts"
    24  	"oss.terrastruct.com/d2/d2renderers/d2svg"
    25  	"oss.terrastruct.com/d2/d2themes/d2themescatalog"
    26  	"oss.terrastruct.com/d2/lib/log"
    27  	"oss.terrastruct.com/d2/lib/textmeasure"
    28  )
    29  
    30  func TestSketch(t *testing.T) {
    31  	t.Parallel()
    32  
    33  	tcs := []testCase{
    34  		{
    35  			name: "basic",
    36  			script: `a -> b
    37  `,
    38  		},
    39  		{
    40  			name: "child to child",
    41  			script: `winter.snow -> summer.sun
    42  		`,
    43  		},
    44  		{
    45  			name:   "elk corners",
    46  			engine: "elk",
    47  			script: `a -> b
    48  b -> c
    49  a -> c
    50  c -> a
    51  		`,
    52  		},
    53  		{
    54  			name: "animated",
    55  			script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
    56  		`,
    57  		},
    58  		{
    59  			name: "connection label",
    60  			script: `a -> b: hello
    61  		`,
    62  		},
    63  		{
    64  			name: "crows feet",
    65  			script: `a1 <-> b1: {
    66  	style.stroke-width: 1
    67  	source-arrowhead: {
    68  		shape: cf-many
    69  	}
    70  	target-arrowhead: {
    71  		shape: cf-many
    72  	}
    73  }
    74  a2 <-> b2: {
    75  	style.stroke-width: 3
    76  	source-arrowhead: {
    77  		shape: cf-many
    78  	}
    79  	target-arrowhead: {
    80  		shape: cf-many
    81  	}
    82  }
    83  a3 <-> b3: {
    84  	style.stroke-width: 6
    85  	source-arrowhead: {
    86  		shape: cf-many
    87  	}
    88  	target-arrowhead: {
    89  		shape: cf-many
    90  	}
    91  }
    92  
    93  c1 <-> d1: {
    94  	style.stroke-width: 1
    95  	source-arrowhead: {
    96  		shape: cf-many-required
    97  	}
    98  	target-arrowhead: {
    99  		shape: cf-many-required
   100  	}
   101  }
   102  c2 <-> d2: {
   103  	style.stroke-width: 3
   104  	source-arrowhead: {
   105  		shape: cf-many-required
   106  	}
   107  	target-arrowhead: {
   108  		shape: cf-many-required
   109  	}
   110  }
   111  c3 <-> d3: {
   112  	style.stroke-width: 6
   113  	source-arrowhead: {
   114  		shape: cf-many-required
   115  	}
   116  	target-arrowhead: {
   117  		shape: cf-many-required
   118  	}
   119  }
   120  
   121  e1 <-> f1: {
   122  	style.stroke-width: 1
   123  	source-arrowhead: {
   124  		shape: cf-one
   125  	}
   126  	target-arrowhead: {
   127  		shape: cf-one
   128  	}
   129  }
   130  e2 <-> f2: {
   131  	style.stroke-width: 3
   132  	source-arrowhead: {
   133  		shape: cf-one
   134  	}
   135  	target-arrowhead: {
   136  		shape: cf-one
   137  	}
   138  }
   139  e3 <-> f3: {
   140  	style.stroke-width: 6
   141  	source-arrowhead: {
   142  		shape: cf-one
   143  	}
   144  	target-arrowhead: {
   145  		shape: cf-one
   146  	}
   147  }
   148  
   149  g1 <-> h1: {
   150  	style.stroke-width: 1
   151  	source-arrowhead: {
   152  		shape: cf-one-required
   153  	}
   154  	target-arrowhead: {
   155  		shape: cf-one-required
   156  	}
   157  }
   158  g2 <-> h2: {
   159  	style.stroke-width: 3
   160  	source-arrowhead: {
   161  		shape: cf-one-required
   162  	}
   163  	target-arrowhead: {
   164  		shape: cf-one-required
   165  	}
   166  }
   167  g3 <-> h3: {
   168  	style.stroke-width: 6
   169  	source-arrowhead: {
   170  		shape: cf-one-required
   171  	}
   172  	target-arrowhead: {
   173  		shape: cf-one-required
   174  	}
   175  }
   176  
   177  c <-> d <-> f: {
   178  	style.stroke-width: 1
   179  	style.stroke: "orange"
   180  	source-arrowhead: {
   181  		shape: cf-many-required
   182  	}
   183  	target-arrowhead: {
   184  		shape: cf-one
   185  	}
   186  }
   187  		`,
   188  		},
   189  		{
   190  			name: "twitter",
   191  			script: `timeline mixer: "" {
   192    explanation: |md
   193      ## **Timeline mixer**
   194      - Inject ads, who-to-follow, onboarding
   195      - Conversation module
   196      - Cursoring,pagination
   197      - Tweat deduplication
   198      - Served data logging
   199    |
   200  }
   201  People discovery: "People discovery \nservice"
   202  admixer: Ad mixer {
   203    style.fill: "#c1a2f3"
   204  }
   205  
   206  onboarding service: "Onboarding \nservice"
   207  timeline mixer -> People discovery
   208  timeline mixer -> onboarding service
   209  timeline mixer -> admixer
   210  container0: "" {
   211    graphql
   212    comment
   213    tlsapi
   214  }
   215  container0.graphql: GraphQL\nFederated Strato Column {
   216    shape: image
   217    icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
   218  }
   219  container0.comment: |md
   220    ## Tweet/user content hydration, visibility filtering
   221  |
   222  container0.tlsapi: TLS-API (being deprecated)
   223  container0.graphql -> timeline mixer
   224  timeline mixer <- container0.tlsapi
   225  twitter fe: "Twitter Frontend " {
   226    icon: https://icons.terrastruct.com/social/013-twitter-1.svg
   227    shape: image
   228  }
   229  twitter fe -> container0.graphql: iPhone web
   230  twitter fe -> container0.tlsapi: HTTP Android
   231  web: Web {
   232    icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
   233    shape: image
   234  }
   235  
   236  Iphone: {
   237    icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
   238    shape: image
   239  }
   240  Android: {
   241    icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
   242    shape: image
   243  }
   244  
   245  web -> twitter fe
   246  timeline scorer: "Timeline\nScorer" {
   247  	style.fill: "#ffdef1"
   248  }
   249  home ranker: Home Ranker
   250  
   251  timeline service: Timeline Service
   252  timeline mixer -> timeline scorer: Thrift RPC
   253  timeline mixer -> home ranker: {
   254    style.stroke-dash: 4
   255    style.stroke: "#000E3D"
   256  }
   257  timeline mixer -> timeline service
   258  home mixer: Home mixer {
   259    # style.fill "#c1a2f3"
   260  }
   261  container0.graphql -> home mixer: {
   262    style.stroke-dash: 4
   263    style.stroke: "#000E3D"
   264  }
   265  home mixer -> timeline scorer
   266  home mixer -> home ranker: {
   267    style.stroke-dash: 4
   268    style.stroke: "#000E3D"
   269  }
   270  home mixer -> timeline service
   271  manhattan 2: Manhattan
   272  gizmoduck: Gizmoduck
   273  socialgraph: Social graph
   274  tweetypie: Tweety Pie
   275  home mixer -> manhattan 2
   276  home mixer -> gizmoduck
   277  home mixer -> socialgraph
   278  home mixer -> tweetypie
   279  Iphone -> twitter fe
   280  Android -> twitter fe
   281  prediction service2: Prediction Service {
   282    shape: image
   283    icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
   284  }
   285  home scorer: Home Scorer {
   286  	style.fill: "#ffdef1"
   287  }
   288  manhattan: Manhattan
   289  memcache: Memcache {
   290    icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
   291  }
   292  
   293  fetch: Fetch {
   294    style.multiple: true
   295    shape: step
   296  }
   297  feature: Feature {
   298    style.multiple: true
   299    shape: step
   300  }
   301  scoring: Scoring {
   302    style.multiple: true
   303    shape: step
   304  }
   305  fetch -> feature
   306  feature -> scoring
   307  
   308  prediction service: Prediction Service {
   309    shape: image
   310    icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
   311  }
   312  scoring -> prediction service
   313  fetch -> container2.crmixer
   314  
   315  home scorer -> manhattan: ""
   316  
   317  home scorer -> memcache: ""
   318  home scorer -> prediction service2
   319  home ranker -> home scorer
   320  home ranker -> container2.crmixer: Candidate Fetch
   321  container2: "" {
   322    style.stroke: "#000E3D"
   323    style.fill: "#ffffff"
   324    crmixer: CrMixer {
   325      style.fill: "#F7F8FE"
   326    }
   327    earlybird: EarlyBird
   328    utag: Utag
   329    space: Space
   330    communities: Communities
   331  }
   332  etc: ...etc
   333  
   334  home scorer -> etc: Feature Hydration
   335  
   336  feature -> manhattan
   337  feature -> memcache
   338  feature -> etc: Candidate sources
   339  		`,
   340  		},
   341  		{
   342  			name: "all_shapes",
   343  			script: `
   344  rectangle: {shape: "rectangle"}
   345  square: {shape: "square"}
   346  page: {shape: "page"}
   347  parallelogram: {shape: "parallelogram"}
   348  document: {shape: "document"}
   349  cylinder: {shape: "cylinder"}
   350  queue: {shape: "queue"}
   351  package: {shape: "package"}
   352  step: {shape: "step"}
   353  callout: {shape: "callout"}
   354  stored_data: {shape: "stored_data"}
   355  person: {shape: "person"}
   356  diamond: {shape: "diamond"}
   357  oval: {shape: "oval"}
   358  circle: {shape: "circle"}
   359  hexagon: {shape: "hexagon"}
   360  cloud: {shape: "cloud"}
   361  
   362  rectangle -> square -> page
   363  parallelogram -> document -> cylinder
   364  queue -> package -> step
   365  callout -> stored_data -> person
   366  diamond -> oval -> circle
   367  hexagon -> cloud
   368  `,
   369  		},
   370  		{
   371  			name: "sql_tables",
   372  			script: `users: {
   373  	shape: sql_table
   374  	id: int
   375  	name: string
   376  	email: string
   377  	password: string
   378  	last_login: datetime
   379  }
   380  
   381  products: {
   382  	shape: sql_table
   383  	id: int
   384  	price: decimal
   385  	sku: string
   386  	name: string
   387  }
   388  
   389  orders: {
   390  	shape: sql_table
   391  	id: int
   392  	user_id: int
   393  	product_id: int
   394  }
   395  
   396  shipments: {
   397  	shape: sql_table
   398  	id: int
   399  	order_id: int
   400  	tracking_number: string {constraint: primary_key}
   401  	status: string
   402  }
   403  
   404  users.id <-> orders.user_id
   405  products.id <-> orders.product_id
   406  shipments.order_id <-> orders.id`,
   407  		},
   408  		{
   409  			name: "class",
   410  			script: `manager: BatchManager {
   411    shape: class
   412    -num: int
   413    -timeout: int
   414    -pid
   415  
   416    +getStatus(): Enum
   417    +getJobs(): "Job[]"
   418    +setTimeout(seconds int)
   419  }
   420  `,
   421  		},
   422  		{
   423  			name: "arrowheads",
   424  			script: `
   425  a: ""
   426  b: ""
   427  a.1 -- b.1: none
   428  a.2 <-> b.2: arrow {
   429  	source-arrowhead.shape: arrow
   430  	target-arrowhead.shape: arrow
   431  }
   432  a.3 <-> b.3: triangle {
   433  	source-arrowhead.shape: triangle
   434  	target-arrowhead.shape: triangle
   435  }
   436  a.4 <-> b.4: diamond {
   437  	source-arrowhead.shape: diamond
   438  	target-arrowhead.shape: diamond
   439  }
   440  a.5 <-> b.5: diamond filled {
   441  	source-arrowhead: {
   442  		shape: diamond
   443  		style.filled: true
   444  	}
   445  	target-arrowhead: {
   446  		shape: diamond
   447  		style.filled: true
   448  	}
   449  }
   450  a.6 <-> b.6: cf-many {
   451  	source-arrowhead.shape: cf-many
   452  	target-arrowhead.shape: cf-many
   453  }
   454  a.7 <-> b.7: cf-many-required {
   455  	source-arrowhead.shape: cf-many-required
   456  	target-arrowhead.shape: cf-many-required
   457  }
   458  a.8 <-> b.8: cf-one {
   459  	source-arrowhead.shape: cf-one
   460  	target-arrowhead.shape: cf-one
   461  }
   462  a.9 <-> b.9: cf-one-required {
   463  	source-arrowhead.shape: cf-one-required
   464  	target-arrowhead.shape: cf-one-required
   465  }
   466  `,
   467  		},
   468  		{
   469  			name: "opacity",
   470  			script: `x.style.opacity: 0.4
   471  y: |md
   472    linux: because a PC is a terrible thing to waste
   473  | {
   474  	style.opacity: 0.4
   475  }
   476  x -> a: {
   477    label: You don't have to know how the computer works,\njust how to work the computer.
   478    style.opacity: 0.4
   479  }
   480  users: {
   481  	shape: sql_table
   482  	last_login: datetime
   483  	style.opacity: 0.4
   484  }
   485  `,
   486  		},
   487  		{
   488  			name: "overlay",
   489  			script: `bright: {
   490  	style.stroke: "#000"
   491  	style.font-color: "#000"
   492  	style.fill: "#fff"
   493  }
   494  normal: {
   495  	style.stroke: "#000"
   496  	style.font-color: "#000"
   497  	style.fill: "#ccc"
   498  }
   499  dark: {
   500  	style.stroke: "#000"
   501  	style.font-color: "#fff"
   502  	style.fill: "#555"
   503  }
   504  darker: {
   505  	style.stroke: "#000"
   506  	style.font-color: "#fff"
   507  	style.fill: "#000"
   508  }
   509  `,
   510  		},
   511  		{
   512  			name:    "terminal",
   513  			themeID: d2themescatalog.Terminal.ID,
   514  			script: `network: {
   515    cell tower: {
   516  		satellites: {
   517  			shape: stored_data
   518        style.multiple: true
   519  		}
   520  
   521  		transmitter
   522  
   523  		satellites -> transmitter: send
   524  		satellites -> transmitter: send
   525  		satellites -> transmitter: send
   526    }
   527  
   528    online portal: {
   529      ui: { shape: hexagon }
   530    }
   531  
   532    data processor: {
   533      storage: {
   534        shape: cylinder
   535        style.multiple: true
   536      }
   537    }
   538  
   539    cell tower.transmitter -> data processor.storage: phone logs
   540  }
   541  
   542  user: {
   543    shape: person
   544    width: 130
   545  }
   546  
   547  user -> network.cell tower: make call
   548  user -> network.online portal.ui: access {
   549    style.stroke-dash: 3
   550  }
   551  
   552  api server -> network.online portal.ui: display
   553  api server -> logs: persist
   554  logs: { shape: page; style.multiple: true }
   555  
   556  network.data processor -> api server
   557  `,
   558  		},
   559  		{
   560  			name:    "basic dark",
   561  			themeID: 200,
   562  			script: `a -> b
   563  `,
   564  		},
   565  		{
   566  			name:    "child to child dark",
   567  			themeID: 200,
   568  			script: `winter.snow -> summer.sun
   569  		`,
   570  		},
   571  		{
   572  			name:    "animated dark",
   573  			themeID: 200,
   574  			script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
   575  		`,
   576  		},
   577  		{
   578  			name:    "connection label dark",
   579  			themeID: 200,
   580  			script: `a -> b: hello
   581  		`,
   582  		},
   583  		{
   584  			name:    "crows feet dark",
   585  			themeID: 200,
   586  			script: `a1 <-> b1: {
   587  	style.stroke-width: 1
   588  	source-arrowhead: {
   589  		shape: cf-many
   590  	}
   591  	target-arrowhead: {
   592  		shape: cf-many
   593  	}
   594  }
   595  a2 <-> b2: {
   596  	style.stroke-width: 3
   597  	source-arrowhead: {
   598  		shape: cf-many
   599  	}
   600  	target-arrowhead: {
   601  		shape: cf-many
   602  	}
   603  }
   604  a3 <-> b3: {
   605  	style.stroke-width: 6
   606  	source-arrowhead: {
   607  		shape: cf-many
   608  	}
   609  	target-arrowhead: {
   610  		shape: cf-many
   611  	}
   612  }
   613  
   614  c1 <-> d1: {
   615  	style.stroke-width: 1
   616  	source-arrowhead: {
   617  		shape: cf-many-required
   618  	}
   619  	target-arrowhead: {
   620  		shape: cf-many-required
   621  	}
   622  }
   623  c2 <-> d2: {
   624  	style.stroke-width: 3
   625  	source-arrowhead: {
   626  		shape: cf-many-required
   627  	}
   628  	target-arrowhead: {
   629  		shape: cf-many-required
   630  	}
   631  }
   632  c3 <-> d3: {
   633  	style.stroke-width: 6
   634  	source-arrowhead: {
   635  		shape: cf-many-required
   636  	}
   637  	target-arrowhead: {
   638  		shape: cf-many-required
   639  	}
   640  }
   641  
   642  e1 <-> f1: {
   643  	style.stroke-width: 1
   644  	source-arrowhead: {
   645  		shape: cf-one
   646  	}
   647  	target-arrowhead: {
   648  		shape: cf-one
   649  	}
   650  }
   651  e2 <-> f2: {
   652  	style.stroke-width: 3
   653  	source-arrowhead: {
   654  		shape: cf-one
   655  	}
   656  	target-arrowhead: {
   657  		shape: cf-one
   658  	}
   659  }
   660  e3 <-> f3: {
   661  	style.stroke-width: 6
   662  	source-arrowhead: {
   663  		shape: cf-one
   664  	}
   665  	target-arrowhead: {
   666  		shape: cf-one
   667  	}
   668  }
   669  
   670  g1 <-> h1: {
   671  	style.stroke-width: 1
   672  	source-arrowhead: {
   673  		shape: cf-one-required
   674  	}
   675  	target-arrowhead: {
   676  		shape: cf-one-required
   677  	}
   678  }
   679  g2 <-> h2: {
   680  	style.stroke-width: 3
   681  	source-arrowhead: {
   682  		shape: cf-one-required
   683  	}
   684  	target-arrowhead: {
   685  		shape: cf-one-required
   686  	}
   687  }
   688  g3 <-> h3: {
   689  	style.stroke-width: 6
   690  	source-arrowhead: {
   691  		shape: cf-one-required
   692  	}
   693  	target-arrowhead: {
   694  		shape: cf-one-required
   695  	}
   696  }
   697  
   698  c <-> d <-> f: {
   699  	style.stroke-width: 1
   700  	style.stroke: "orange"
   701  	source-arrowhead: {
   702  		shape: cf-many-required
   703  	}
   704  	target-arrowhead: {
   705  		shape: cf-one
   706  	}
   707  }
   708  		`,
   709  		},
   710  		{
   711  			name:    "twitter dark",
   712  			themeID: 200,
   713  			script: `timeline mixer: "" {
   714    explanation: |md
   715      ## **Timeline mixer**
   716      - Inject ads, who-to-follow, onboarding
   717      - Conversation module
   718      - Cursoring,pagination
   719      - Tweat deduplication
   720      - Served data logging
   721    |
   722  }
   723  People discovery: "People discovery \nservice"
   724  admixer: Ad mixer {
   725    style.fill: "#c1a2f3"
   726  }
   727  
   728  onboarding service: "Onboarding \nservice"
   729  timeline mixer -> People discovery
   730  timeline mixer -> onboarding service
   731  timeline mixer -> admixer
   732  container0: "" {
   733    graphql
   734    comment
   735    tlsapi
   736  }
   737  container0.graphql: GraphQL\nFederated Strato Column {
   738    shape: image
   739    icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
   740  }
   741  container0.comment: |md
   742    ## Tweet/user content hydration, visibility filtering
   743  |
   744  container0.tlsapi: TLS-API (being deprecated)
   745  container0.graphql -> timeline mixer
   746  timeline mixer <- container0.tlsapi
   747  twitter fe: "Twitter Frontend " {
   748    icon: https://icons.terrastruct.com/social/013-twitter-1.svg
   749    shape: image
   750  }
   751  twitter fe -> container0.graphql: iPhone web
   752  twitter fe -> container0.tlsapi: HTTP Android
   753  web: Web {
   754    icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
   755    shape: image
   756  }
   757  
   758  Iphone: {
   759    icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
   760    shape: image
   761  }
   762  Android: {
   763    icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
   764    shape: image
   765  }
   766  
   767  web -> twitter fe
   768  timeline scorer: "Timeline\nScorer" {
   769  	style.fill: "#ffdef1"
   770  }
   771  home ranker: Home Ranker
   772  
   773  timeline service: Timeline Service
   774  timeline mixer -> timeline scorer: Thrift RPC
   775  timeline mixer -> home ranker: {
   776    style.stroke-dash: 4
   777    style.stroke: "#000E3D"
   778  }
   779  timeline mixer -> timeline service
   780  home mixer: Home mixer {
   781    # style.fill "#c1a2f3"
   782  }
   783  container0.graphql -> home mixer: {
   784    style.stroke-dash: 4
   785    style.stroke: "#000E3D"
   786  }
   787  home mixer -> timeline scorer
   788  home mixer -> home ranker: {
   789    style.stroke-dash: 4
   790    style.stroke: "#000E3D"
   791  }
   792  home mixer -> timeline service
   793  manhattan 2: Manhattan
   794  gizmoduck: Gizmoduck
   795  socialgraph: Social graph
   796  tweetypie: Tweety Pie
   797  home mixer -> manhattan 2
   798  home mixer -> gizmoduck
   799  home mixer -> socialgraph
   800  home mixer -> tweetypie
   801  Iphone -> twitter fe
   802  Android -> twitter fe
   803  prediction service2: Prediction Service {
   804    shape: image
   805    icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
   806  }
   807  home scorer: Home Scorer {
   808  	style.fill: "#ffdef1"
   809  }
   810  manhattan: Manhattan
   811  memcache: Memcache {
   812    icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
   813  }
   814  
   815  fetch: Fetch {
   816    style.multiple: true
   817    shape: step
   818  }
   819  feature: Feature {
   820    style.multiple: true
   821    shape: step
   822  }
   823  scoring: Scoring {
   824    style.multiple: true
   825    shape: step
   826  }
   827  fetch -> feature
   828  feature -> scoring
   829  
   830  prediction service: Prediction Service {
   831    shape: image
   832    icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
   833  }
   834  scoring -> prediction service
   835  fetch -> container2.crmixer
   836  
   837  home scorer -> manhattan: ""
   838  
   839  home scorer -> memcache: ""
   840  home scorer -> prediction service2
   841  home ranker -> home scorer
   842  home ranker -> container2.crmixer: Candidate Fetch
   843  container2: "" {
   844    style.stroke: "#000E3D"
   845    style.fill: "#ffffff"
   846    crmixer: CrMixer {
   847      style.fill: "#F7F8FE"
   848    }
   849    earlybird: EarlyBird
   850    utag: Utag
   851    space: Space
   852    communities: Communities
   853  }
   854  etc: ...etc
   855  
   856  home scorer -> etc: Feature Hydration
   857  
   858  feature -> manhattan
   859  feature -> memcache
   860  feature -> etc: Candidate sources
   861  		`,
   862  		},
   863  		{
   864  			name:    "all_shapes dark",
   865  			themeID: 200,
   866  			script: `
   867  rectangle: {shape: "rectangle"}
   868  square: {shape: "square"}
   869  page: {shape: "page"}
   870  parallelogram: {shape: "parallelogram"}
   871  document: {shape: "document"}
   872  cylinder: {shape: "cylinder"}
   873  queue: {shape: "queue"}
   874  package: {shape: "package"}
   875  step: {shape: "step"}
   876  callout: {shape: "callout"}
   877  stored_data: {shape: "stored_data"}
   878  person: {shape: "person"}
   879  diamond: {shape: "diamond"}
   880  oval: {shape: "oval"}
   881  circle: {shape: "circle"}
   882  hexagon: {shape: "hexagon"}
   883  cloud: {shape: "cloud"}
   884  
   885  rectangle -> square -> page
   886  parallelogram -> document -> cylinder
   887  queue -> package -> step
   888  callout -> stored_data -> person
   889  diamond -> oval -> circle
   890  hexagon -> cloud
   891  `,
   892  		},
   893  		{
   894  			name:    "sql_tables dark",
   895  			themeID: 200,
   896  			script: `users: {
   897  	shape: sql_table
   898  	id: int
   899  	name: string
   900  	email: string
   901  	password: string
   902  	last_login: datetime
   903  }
   904  
   905  products: {
   906  	shape: sql_table
   907  	id: int
   908  	price: decimal
   909  	sku: string
   910  	name: string
   911  }
   912  
   913  orders: {
   914  	shape: sql_table
   915  	id: int
   916  	user_id: int
   917  	product_id: int
   918  }
   919  
   920  shipments: {
   921  	shape: sql_table
   922  	id: int
   923  	order_id: int
   924  	tracking_number: string {constraint: primary_key}
   925  	status: string
   926  }
   927  
   928  users.id <-> orders.user_id
   929  products.id <-> orders.product_id
   930  shipments.order_id <-> orders.id`,
   931  		},
   932  		{
   933  			name:    "class dark",
   934  			themeID: 200,
   935  			script: `manager: BatchManager {
   936    shape: class
   937    -num: int
   938    -timeout: int
   939    -pid
   940  
   941    +getStatus(): Enum
   942    +getJobs(): "Job[]"
   943    +setTimeout(seconds int)
   944  }
   945  `,
   946  		},
   947  		{
   948  			name:    "arrowheads dark",
   949  			themeID: 200,
   950  			script: `
   951  a: ""
   952  b: ""
   953  a.1 -- b.1: none
   954  a.2 <-> b.2: arrow {
   955  	source-arrowhead.shape: arrow
   956  	target-arrowhead.shape: arrow
   957  }
   958  a.3 <-> b.3: triangle {
   959  	source-arrowhead.shape: triangle
   960  	target-arrowhead.shape: triangle
   961  }
   962  a.4 <-> b.4: diamond {
   963  	source-arrowhead.shape: diamond
   964  	target-arrowhead.shape: diamond
   965  }
   966  a.5 <-> b.5: diamond filled {
   967  	source-arrowhead: {
   968  		shape: diamond
   969  		style.filled: true
   970  	}
   971  	target-arrowhead: {
   972  		shape: diamond
   973  		style.filled: true
   974  	}
   975  }
   976  a.6 <-> b.6: cf-many {
   977  	source-arrowhead.shape: cf-many
   978  	target-arrowhead.shape: cf-many
   979  }
   980  a.7 <-> b.7: cf-many-required {
   981  	source-arrowhead.shape: cf-many-required
   982  	target-arrowhead.shape: cf-many-required
   983  }
   984  a.8 <-> b.8: cf-one {
   985  	source-arrowhead.shape: cf-one
   986  	target-arrowhead.shape: cf-one
   987  }
   988  a.9 <-> b.9: cf-one-required {
   989  	source-arrowhead.shape: cf-one-required
   990  	target-arrowhead.shape: cf-one-required
   991  }
   992  `,
   993  		},
   994  		{
   995  			name:    "opacity dark",
   996  			themeID: 200,
   997  			script: `x.style.opacity: 0.4
   998  y: |md
   999    linux: because a PC is a terrible thing to waste
  1000  | {
  1001  	style.opacity: 0.4
  1002  }
  1003  x -> a: {
  1004    label: You don't have to know how the computer works,\njust how to work the computer.
  1005    style.opacity: 0.4
  1006  }
  1007  users: {
  1008  	shape: sql_table
  1009  	last_login: datetime
  1010  	style.opacity: 0.4
  1011  }
  1012  `,
  1013  		},
  1014  		{
  1015  			name: "root-fill",
  1016  			script: `style.fill: honeydew
  1017  style.stroke: LightSteelBlue
  1018  style.double-border: true
  1019  
  1020  title: Flow-I (Warehousing, Installation) {
  1021    near: top-center
  1022    shape: text
  1023    style: {
  1024      font-size: 24
  1025      bold: false
  1026      underline: false
  1027    }
  1028  }
  1029  OEM Factory
  1030  OEM Factory -> OEM Warehouse
  1031  OEM Factory -> Distributor Warehouse
  1032  OEM Factory -> company Warehouse
  1033  
  1034  company Warehouse.Master -> company Warehouse.Regional-1
  1035  company Warehouse.Master -> company Warehouse.Regional-2
  1036  company Warehouse.Master -> company Warehouse.Regional-N
  1037  company Warehouse.Regional-1 -> company Warehouse.Regional-2
  1038  company Warehouse.Regional-2 -> company Warehouse.Regional-N
  1039  company Warehouse.Regional-N -> company Warehouse.Regional-1
  1040  
  1041  company Warehouse.explanation: |md
  1042    ### company Warehouse
  1043    - Asset Tagging
  1044    - Inventory
  1045    - Staging
  1046    - Dispatch to Site
  1047  |
  1048  `,
  1049  		},
  1050  		{
  1051  			name: "double-border",
  1052  			script: `a: {
  1053    style.double-border: true
  1054    b
  1055  }
  1056  c: {
  1057    shape: oval
  1058    style.double-border: true
  1059    d
  1060  }
  1061  normal: {
  1062    nested normal
  1063  }
  1064  something
  1065  `,
  1066  		},
  1067  		{
  1068  			name: "class_and_sqlTable_border_radius",
  1069  			script: `
  1070  				a: {
  1071  					shape: sql_table
  1072  					id: int {constraint: primary_key}
  1073  					disk: int {constraint: foreign_key}
  1074  
  1075  					json: jsonb  {constraint: unique}
  1076  					last_updated: timestamp with time zone
  1077  
  1078  					style: {
  1079  						fill: red
  1080  						border-radius: 0
  1081  					}
  1082  				}
  1083  
  1084  				b: {
  1085  					shape: class
  1086  
  1087  					field: "[]string"
  1088  					method(a uint64): (x, y int)
  1089  
  1090  					style: {
  1091  						border-radius: 0
  1092  					}
  1093  				}
  1094  
  1095  				c: {
  1096  					shape: class
  1097  					style: {
  1098  						border-radius: 0
  1099  					}
  1100  				}
  1101  
  1102  				d: {
  1103  					shape: sql_table
  1104  					style: {
  1105  						border-radius: 0
  1106  					}
  1107  				}
  1108  			`,
  1109  		},
  1110  		{
  1111  			name: "paper-real",
  1112  			script: `style.fill-pattern: paper
  1113  style.fill: "#947A6D"
  1114  NETWORK: {
  1115    style: {
  1116  	  stroke: black
  1117      fill-pattern: dots
  1118      double-border: true
  1119      fill: "#E7E9EE"
  1120      font: mono
  1121    }
  1122    CELL TOWER: {
  1123  		style: {
  1124  			stroke: black
  1125  			fill-pattern: dots
  1126  			fill: "#F5F6F9"
  1127  			font: mono
  1128  		}
  1129  		satellites: SATELLITES {
  1130  			shape: stored_data
  1131  			style: {
  1132  				font: mono
  1133  				fill: white
  1134  				stroke: black
  1135  				multiple: true
  1136  			}
  1137  		}
  1138  
  1139  		transmitter: TRANSMITTER {
  1140  			style: {
  1141  				font: mono
  1142  				fill: white
  1143  				stroke: black
  1144  			}
  1145  		}
  1146  
  1147  		satellites -> transmitter: SEND {
  1148  			style.stroke: black
  1149  			style.font: mono
  1150  		}
  1151  		satellites -> transmitter: SEND {
  1152  			style.stroke: black
  1153  			style.font: mono
  1154  		}
  1155  		satellites -> transmitter: SEND {
  1156  			style.stroke: black
  1157  			style.font: mono
  1158  		}
  1159    }
  1160  }
  1161  `},
  1162  		{
  1163  			name: "dots-real",
  1164  			script: `
  1165  NETWORK: {
  1166    style: {
  1167  	  stroke: black
  1168      fill-pattern: dots
  1169      double-border: true
  1170      fill: "#E7E9EE"
  1171      font: mono
  1172    }
  1173    CELL TOWER: {
  1174  		style: {
  1175  			stroke: black
  1176  			fill-pattern: dots
  1177  			fill: "#F5F6F9"
  1178  			font: mono
  1179  		}
  1180  		satellites: SATELLITES {
  1181  			shape: stored_data
  1182  			style: {
  1183  				font: mono
  1184  				fill: white
  1185  				stroke: black
  1186  				multiple: true
  1187  			}
  1188  		}
  1189  
  1190  		transmitter: TRANSMITTER {
  1191  			style: {
  1192  				font: mono
  1193  				fill: white
  1194  				stroke: black
  1195  			}
  1196  		}
  1197  
  1198  		satellites -> transmitter: SEND {
  1199  			style.stroke: black
  1200  			style.font: mono
  1201  		}
  1202  		satellites -> transmitter: SEND {
  1203  			style.stroke: black
  1204  			style.font: mono
  1205  		}
  1206  		satellites -> transmitter: SEND {
  1207  			style.stroke: black
  1208  			style.font: mono
  1209  		}
  1210    }
  1211  }
  1212  D2 Parser: {
  1213  	style.fill-pattern: grain
  1214    shape: class
  1215  
  1216    +reader: io.RuneReader
  1217    # Default visibility is + so no need to specify.
  1218    readerPos: d2ast.Position
  1219  
  1220    # Private field.
  1221    -lookahead: "[]rune"
  1222  
  1223    # Escape the # to prevent being parsed as comment
  1224    #lookaheadPos: d2ast.Position
  1225    # Or just wrap in quotes
  1226    "#peekn(n int)": (s string, eof bool)
  1227  
  1228    +peek(): (r rune, eof bool)
  1229    rewind()
  1230    commit()
  1231  }
  1232  `,
  1233  		},
  1234  		{
  1235  			name: "dots-3d",
  1236  			script: `x: {style.3d: true; style.fill-pattern: dots}
  1237  y: {shape: hexagon; style.3d: true; style.fill-pattern: dots}
  1238  `,
  1239  		},
  1240  		{
  1241  			name: "dots-multiple",
  1242  			script: `
  1243  rectangle: {shape: "rectangle"; style.fill-pattern: dots; style.multiple: true}
  1244  square: {shape: "square"; style.fill-pattern: dots; style.multiple: true}
  1245  page: {shape: "page"; style.fill-pattern: dots; style.multiple: true}
  1246  parallelogram: {shape: "parallelogram"; style.fill-pattern: dots; style.multiple: true}
  1247  document: {shape: "document"; style.fill-pattern: dots; style.multiple: true}
  1248  cylinder: {shape: "cylinder"; style.fill-pattern: dots; style.multiple: true}
  1249  queue: {shape: "queue"; style.fill-pattern: dots; style.multiple: true}
  1250  package: {shape: "package"; style.fill-pattern: dots; style.multiple: true}
  1251  step: {shape: "step"; style.fill-pattern: dots; style.multiple: true}
  1252  callout: {shape: "callout"; style.fill-pattern: dots; style.multiple: true}
  1253  stored_data: {shape: "stored_data"; style.fill-pattern: dots; style.multiple: true}
  1254  person: {shape: "person"; style.fill-pattern: dots; style.multiple: true}
  1255  diamond: {shape: "diamond"; style.fill-pattern: dots; style.multiple: true}
  1256  oval: {shape: "oval"; style.fill-pattern: dots; style.multiple: true}
  1257  circle: {shape: "circle"; style.fill-pattern: dots; style.multiple: true}
  1258  hexagon: {shape: "hexagon"; style.fill-pattern: dots; style.multiple: true}
  1259  cloud: {shape: "cloud"; style.fill-pattern: dots; style.multiple: true}
  1260  
  1261  rectangle -> square -> page
  1262  parallelogram -> document -> cylinder
  1263  queue -> package -> step
  1264  callout -> stored_data -> person
  1265  diamond -> oval -> circle
  1266  hexagon -> cloud
  1267  `,
  1268  		},
  1269  		{
  1270  			name: "dots-all",
  1271  			script: `
  1272  rectangle: {shape: "rectangle"; style.fill-pattern: dots}
  1273  square: {shape: "square"; style.fill-pattern: dots}
  1274  page: {shape: "page"; style.fill-pattern: dots}
  1275  parallelogram: {shape: "parallelogram"; style.fill-pattern: dots}
  1276  document: {shape: "document"; style.fill-pattern: dots}
  1277  cylinder: {shape: "cylinder"; style.fill-pattern: dots}
  1278  queue: {shape: "queue"; style.fill-pattern: dots}
  1279  package: {shape: "package"; style.fill-pattern: dots}
  1280  step: {shape: "step"; style.fill-pattern: dots}
  1281  callout: {shape: "callout"; style.fill-pattern: dots}
  1282  stored_data: {shape: "stored_data"; style.fill-pattern: dots}
  1283  person: {shape: "person"; style.fill-pattern: dots}
  1284  diamond: {shape: "diamond"; style.fill-pattern: dots}
  1285  oval: {shape: "oval"; style.fill-pattern: dots}
  1286  circle: {shape: "circle"; style.fill-pattern: dots}
  1287  hexagon: {shape: "hexagon"; style.fill-pattern: dots}
  1288  cloud: {shape: "cloud"; style.fill-pattern: dots}
  1289  
  1290  rectangle -> square -> page
  1291  parallelogram -> document -> cylinder
  1292  queue -> package -> step
  1293  callout -> stored_data -> person
  1294  diamond -> oval -> circle
  1295  hexagon -> cloud
  1296  `,
  1297  		},
  1298  		{
  1299  			name: "long_arrowhead_label",
  1300  			script: `
  1301  a -> b: {
  1302  	target-arrowhead: "a to b with unexpectedly long target arrowhead label"
  1303  }
  1304  `,
  1305  		},
  1306  		{
  1307  			name: "unfilled_triangle",
  1308  			script: `
  1309  direction: right
  1310  
  1311  A <-> B: default {
  1312    source-arrowhead.style.filled: false
  1313    target-arrowhead.style.filled: false
  1314  }
  1315  C <-> D: triangle {
  1316    source-arrowhead: {
  1317      shape: triangle
  1318      style.filled: false
  1319    }
  1320    target-arrowhead: {
  1321      shape: triangle
  1322      style.filled: false
  1323    }
  1324  }`,
  1325  		},
  1326  	}
  1327  	runa(t, tcs)
  1328  }
  1329  
  1330  type testCase struct {
  1331  	name    string
  1332  	themeID int64
  1333  	script  string
  1334  	skip    bool
  1335  	engine  string
  1336  }
  1337  
  1338  func runa(t *testing.T, tcs []testCase) {
  1339  	for _, tc := range tcs {
  1340  		tc := tc
  1341  		t.Run(tc.name, func(t *testing.T) {
  1342  			if tc.skip {
  1343  				t.Skip()
  1344  			}
  1345  			t.Parallel()
  1346  
  1347  			run(t, tc)
  1348  		})
  1349  	}
  1350  }
  1351  
  1352  func run(t *testing.T, tc testCase) {
  1353  	ctx := context.Background()
  1354  	ctx = log.WithTB(ctx, t, nil)
  1355  	ctx = log.Leveled(ctx, slog.LevelDebug)
  1356  
  1357  	ruler, err := textmeasure.NewRuler()
  1358  	if !tassert.Nil(t, err) {
  1359  		return
  1360  	}
  1361  
  1362  	layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
  1363  		if strings.EqualFold(engine, "elk") {
  1364  			return d2elklayout.DefaultLayout, nil
  1365  		}
  1366  		return d2dagrelayout.DefaultLayout, nil
  1367  	}
  1368  	renderOpts := &d2svg.RenderOpts{
  1369  		Sketch:  go2.Pointer(true),
  1370  		ThemeID: go2.Pointer(tc.themeID),
  1371  	}
  1372  	diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
  1373  		Ruler:          ruler,
  1374  		Layout:         &tc.engine,
  1375  		LayoutResolver: layoutResolver,
  1376  		FontFamily:     go2.Pointer(d2fonts.HandDrawn),
  1377  	}, renderOpts)
  1378  	if !tassert.Nil(t, err) {
  1379  		return
  1380  	}
  1381  
  1382  	dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestSketch/"))
  1383  	pathGotSVG := filepath.Join(dataPath, "sketch.got.svg")
  1384  
  1385  	svgBytes, err := d2svg.Render(diagram, renderOpts)
  1386  	assert.Success(t, err)
  1387  	err = os.MkdirAll(dataPath, 0755)
  1388  	assert.Success(t, err)
  1389  	err = os.WriteFile(pathGotSVG, svgBytes, 0600)
  1390  	assert.Success(t, err)
  1391  	defer os.Remove(pathGotSVG)
  1392  
  1393  	var xmlParsed interface{}
  1394  	err = xml.Unmarshal(svgBytes, &xmlParsed)
  1395  	assert.Success(t, err)
  1396  
  1397  	// We want the visual diffs to compare, but there's floating point precision differences between CI and user machines, so don't compare raw strings
  1398  	err = diff.Testdata(filepath.Join(dataPath, "sketch"), ".svg", svgBytes)
  1399  	assert.Success(t, err)
  1400  }
  1401  

View as plain text